订单防重复提交:token 发放以及校验

news2024/12/26 22:58:04

订单防重复提交:token 发放以及校验

  • 1. 基于Token校验避免订单重复提交

1. 基于Token校验避免订单重复提交

在很多秒杀场景中,用户为了能下单成功,会频繁的点击下单按钮,这时候如果没有做好控制的话,就可能会给一个用户创建重复订单。

如何防止这个问题呢?

其实有一个好办法,那就是用户在下单的时候,带一个 token 过来,我们校验这个 token 的有效性,如果 token 有效,则允许下单,如果无效,则不允许用户下单。

注意注意注意:这里的 token 和 sa-token(鉴权token) 这个框架中的 token 不是一回事儿,也没有任何关系。
sa-token 里面的那个 token是用于登录鉴权的。
而这里的 token 是用来防止订单重复提交的,他俩不是一个 token,这里的 token 也不是 sa-token 发放的,而是我们自己实现的一个发放和存储,以及后续的校验,都是我们自己做的。

在这里插入图片描述

那么,这个 token 是如何发放和校验的的呢?
token 的发放比较简单,我们定义一个 controller,在下单页面渲染的时候从接口中获取一下就行了。

package cn.hollis.nft.turbo.web.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;

public class TokenFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(TokenFilter.class);

    public static final ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>();

    private RedissonClient redissonClient;

    public TokenFilter(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 过滤器初始化,可选实现
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;

            // 从请求头中获取Token
            String token = httpRequest.getHeader("Authorization");
            logger.info("TokenFilter::doFilter,httpRequest:{}", httpRequest);
            logger.info("TokenFilter::doFilter,token:{}", token);

            if (token == null || "null".equals(token) || "undefined".equals(token)) {
                httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                httpResponse.getWriter().write("No Token Found ...");
                logger.error("no token found in header , pls check!");
                return;
            }

            // 校验Token的有效性
            boolean isValid = checkTokenValidity(token);

            if (!isValid) {
                httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                httpResponse.getWriter().write("Invalid or expired token");
                logger.error("token validate failed , pls check!");
                return;
            }

            // Token有效,继续执行其他过滤器链
            chain.doFilter(request, response);
        } finally {
            tokenThreadLocal.remove();
        }
    }

    /**
     * 检查Token的有效性
     * 通过Redis判断Token是否存在,并将其删除
     * 这个方法用于确保Token的单次使用,增强安全性
     *
     * @param token 要检查的Token
     * @return 如果Token在Redis中存在,则返回true;否则返回false
     */
    private boolean checkTokenValidity(String token) {
        // Lua脚本,用于获取并删除Redis中的Token
        // 这样做是为了保证Token的单次使用,增强安全性
        String luaScript = """
                local value = redis.call('GET', KEYS[1])
                redis.call('DEL', KEYS[1])
                return value""";

        // 6.2.3以上可以直接使用GETDEL命令
        // String value = (String) redisTemplate.opsForValue().getAndDelete(token);

        // 使用Redisson客户端执行Lua脚本,判断Token是否存在并将其删除
        // KEYS[1]表示脚本中的Token键
        // getScript这个方法用于获取一个脚本对象(通常用于处理 Redis 中的脚本相关操作)。
        // 在 Redis 中,可以使用 Lua 脚本进行复杂的操作,例如原子性地执行多个命令等。Redisson 通过这个方法提供了对脚本操作的支持。
        // eval 具体来说,它会将脚本发送到 Redis 服务器端执行,并且可以根据脚本的逻辑在
        // Redis 中进行数据操作(如读取、写入、修改数据等),脚本执行的结果会被返回给调用者(在 Java 中就是返回给执行eval()方法的地方)。
        // 例如,如果脚本是一个用于计算某个键对应的值的两倍的 Lua 脚本,eval()方法执行这个脚本后就会得到计算后的结果并返回。
        String result = (String) redissonClient.getScript().eval(RScript.Mode.READ_WRITE,
                luaScript,
                RScript.ReturnType.STATUS,
                Arrays.asList(token));

        // 将Redis中的返回值存储到ThreadLocal中,以便在当前线程内其他地方使用
        tokenThreadLocal.set(result);
        // 如果result不为空,说明Token在Redis中存在过,即Token有效
        return result != null;
    }

    @Override
    public void destroy() {
    }
}


主要实现在doFilter方法中,主要是判断请求中是否携带了 token,如果携带了,通过 redis 校验 token 是否有效,如果有效,则把这个 token 删除,并且放过请求。如果无效,则直接拒绝请求。

这里的token 校验及移除,我们是通过 lua 脚本实现的,保证原子性。

有了这个 filter 之后,我们需要让他能够生效,则需要以下配置:

import cn.hollis.nft.turbo.web.filter.TokenFilter;
import cn.hollis.nft.turbo.web.handler.GlobalWebExceptionHandler;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类,用于配置全局异常处理器和Token过滤器
 *
 * @AutoConfiguration注解
 * 来源与功能概述
 * 在 Spring Boot 框架中,@AutoConfiguration是一个非常重要的注解。它用于标识一个类是自动配置类。
 * Spring Boot 的自动配置机制会根据类路径中的依赖和预定义的配置条件自动加载和应用这些自动配置类。
 * 工作原理
 * 自动配置类通常包含一系列的@Bean方法,这些方法会向 Spring 容器中注入各种组件。
 * 例如,当项目的类路径中存在某些特定的库(如数据库驱动)时,相关的自动配置类就会被触发,
 * 它内部的@Bean方法会创建和配置与该库相关的组件,如数据源、事务管理器等,从而减少了开发者手动配置这些组件的工作量。
 *
 * @ConditionalOnWebApplication注解
 * 条件注解类型
 * 这是 Spring Boot 中的一个条件注解。条件注解用于根据特定的条件来决定是否加载某个配置类或者某个@Bean方法。
 * 针对 Web 应用的条件判断
 * @ConditionalOnWebApplication用于判断当前应用是否是一个 Web 应用。它有不同的匹配模式:
 * 如果没有指定任何模式,只要是 Web 应用(无论是 Servlet - based 还是 Reactive - based)就会满足条件。
 * type = ConditionalOnWebApplication.Type.SERVLET:这种模式下,只有当应用是基于 Servlet 的 Web 应用时才会满足条件。例如,在传统的 Spring MVC 应用中,
 * 使用 Servlet 容器(如 Tomcat、Jetty 等)来处理 HTTP 请求,就属于这种情况。
 * type = ConditionalOnWebApplication.Type.REACTIVE:只有当应用是基于响应式(Reactive)的 Web 应用时才满足条件,如使用 Spring WebFlux 构建的应用,
 * 它采用响应式编程模型来处理 HTTP 请求。
 * 这些注解共同作用,使得 Spring Boot 能够根据应用的实际情况(如是否为 Web 应用等)智能地加载和配置相关的组件,提高了应用开发的效率和灵活性。
 *
 * @author Hollis
 */
@AutoConfiguration
@ConditionalOnWebApplication
public class WebConfiguration implements WebMvcConfigurer {

    /**
     * 配置全局Web异常处理处理器
     *
     * @return GlobalWebExceptionHandler 全局异常处理器实例
     */
    @Bean
    @ConditionalOnMissingBean
    GlobalWebExceptionHandler globalWebExceptionHandler() {
        return new GlobalWebExceptionHandler();
    }

    /**
     * 注册Token过滤器
     *
     * @param redissonClient Redisson客户端实例,用于分布式锁等Redis操作
     * @return FilterRegistrationBean<TokenFilter> 注册的Token过滤器实例
     */
    @Bean
    public FilterRegistrationBean<TokenFilter> tokenFilter(RedissonClient redissonClient) {
        FilterRegistrationBean<TokenFilter> registrationBean = new FilterRegistrationBean<>();

        // 初始化TokenFilter实例并设置Redisson客户端
        registrationBean.setFilter(new TokenFilter(redissonClient));
        // 设置过滤器处理的URL模式
        registrationBean.addUrlPatterns("/trade/buy");
        // 设置过滤器顺序
        registrationBean.setOrder(10);

        return registrationBean;
    }

}

这里,我们并不是给所有的页面都加这个 token 的校验,其实很多接口是不需要的,所以我们只需要通过registrationBean.addUrlPatterns(“/trade/buy”);设置上我们需要校验的路径就行了。

前端代码:

latestCollectionCreateOrder() {
				var that = this;
				this.$u.post('/trade/buy', {
					goodsId: that.collectionId,
					goodsType: 'COLLECTION',
					itemCount: 1,
					token: that.checkToken
				}, {
					Authorization: that.checkToken
				}).then(res => {
					if(res.success) {
						that.orderId = res.data;
						that.showPayModalFlag = true;
					}else{
						uni.showToast({
							icon: 'error',
							title: res.message,
							duration: 2000
						});
					}
					
				}).catch(error => {
					if (error.statusCode === 401) {
						uni.showModal({
							title: '请勿重复提交',
							content: ' 请刷新页面重新发起请求',
							showCancel: false,
							success: function (res) {
								if (res.confirm) {
									// Handle the case when user confirms the modal
									// For example, you can redirect to the login page
								}
							}
						});
					} else {
						// Handle other errors
						uni.showToast({
							icon: 'error',
							title: 'An error occurred',
							duration: 2000
						});
					}
					
				});
			},

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2140779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ElementUI 布局——行与列的灵活运用

ElementUI 布局——行与列的灵活运用 一 . 使用 Layout 组件1.1 注册路由1.2 使用 Layout 组件 二 . 行属性2.1 栅格的间隔2.2 自定义元素标签 三 . 列属性3.1 列的偏移3.2 列的移动 在现代网页设计中&#xff0c;布局是构建用户界面的基石。Element UI 框架通过其强大的 <e…

面向对象程序设计之继承(C++)

1.继承的定义 1.1继承的概念 继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段&#xff0c;它允许我们在保持原有类特性的基础上进⾏扩展&#xff0c;增加⽅法(成员函数)和属性(成员变量)&#xff0c;这样产⽣新的类&#xff0c;称派⽣类。继承 呈现了⾯向…

Day26_0.1基础学习MATLAB学习小技巧总结(26)——数据插值

利用空闲时间把碎片化的MATLAB知识重新系统的学习一遍&#xff0c;为了在这个过程中加深印象&#xff0c;也为了能够有所足迹&#xff0c;我会把自己的学习总结发在专栏中&#xff0c;以便学习交流。 参考书目&#xff1a; 1、《MATLAB基础教程 (第三版) (薛山)》 2、《MATL…

Delphi CxGrid的主从表显示设置

界面编辑建立两个不同级别的视图层级-Layout 其实这是一个主从表关系&#xff0c; 1&#xff1a;填好主表的keyfieldnames 2&#xff1a;填好从表的keyfieldnames 3&#xff1a;填好从表的 detaikeyfieldNames与masterkeyfieldnames 4: 从表的数据源一定要按与主表关联的…

Vue实用操作-2-如何使用网页开发者工具

第一步&#xff0c;添加扩展&#xff0c;live服务器 第二步&#xff0c;将 favicon.ico 文件加入到根目录下 第三步&#xff0c;选择以服务器方式运行&#xff0c;并打开浏览器 第四步&#xff0c;在极简插件你中找到 vue 对应插件&#xff0c;安装到扩展插件中 第五步&#xf…

通过hosts.allow和hosts.deny限制用户登录

1、Hosts.allow和host.deny说明 两个文件是控制远程访问设置的&#xff0c;通过设置这个文件可以允许或者拒绝某个ip或者ip段的客户访问linux的某项服务。如果请求访问的主机名或IP不包含在/etc/hosts.allow中&#xff0c;那么tcpd进程就检查/etc/hosts.deny。看请求访问的主机…

【南方科技大学】CS315 Computer Security 【Lab2 Buffer Overflow】

目录 引言软件要求启动虚拟机环境设置禁用地址空间布局随机化&#xff08;ASLR&#xff09;设置编译器标志以禁用安全功能 概述BOF.ctestShellCode.c解释 createBadfile.c 开始利用漏洞在堆栈上查找返回地址 实验2的作业 之前有写过一个 博客&#xff0c;大家可以先看看栈溢出…

Qt ORM模块使用说明

附源码&#xff1a;QxOrm是一个C库资源-CSDN文库 使用说明 把QyOrm文件夹拷贝到自己的工程项目下, 在自己项目里的Pro文件里添加include($$PWD/QyOrm/QyOrm.pri)就能使用了 示例test_qyorm.h写了表的定义,Test_QyOrm_Main.cpp中写了所有支持的功能的例子: 通过自动表单添加…

C++——异常处理机制(try/catch/throw)

一、什么是异常处理机制 C++中的异常处理机制是一种用来检测和处理程序执行期间可能存在的异常情况的技术。它允许开发者编写健壮的代码,能够提前预判和处理程序执行可能会出现的错误,保证程序正常执行,而不会导致程序崩溃。 C++异常处理主要由几个关键字组成: try、cat…

C++笔记之std::map的实用操作

C++笔记之std::map的实用操作 code review 文章目录 C++笔记之std::map的实用操作1.初始化1.1.使用列表初始化1.2.使用 `insert` 方法1.3.使用 `emplace` 方法1.4.复制构造1.5.移动构造2.赋值2.1.列表赋值2.2.插入元素2.3.批量插入3.取值3.1.使用 `[]` 操作符3.2.使用 `at()` …

Vue路由配置、网络请求访问框架项目、element组件介绍学习

系列文章目录 第一章 基础知识、数据类型学习 第二章 万年历项目 第三章 代码逻辑训练习题 第四章 方法、数组学习 第五章 图书管理系统项目 第六章 面向对象编程&#xff1a;封装、继承、多态学习 第七章 封装继承多态习题 第八章 常用类、包装类、异常处理机制学习 第九章 集…

回归预测|基于开普勒优化相关向量机的数据回归预测Matlab程序KOA-RVM 多特征输入单输出 含基础RVM

回归预测|基于开普勒优化相关向量机的数据回归预测Matlab程序KOA-RVM 多特征输入单输出 含基础RVM 文章目录 一、基本原理1. **相关向量机&#xff08;RVM&#xff09;**2. **开普勒优化算法&#xff08;KOA&#xff09;**3. **KOA-RVM回归预测模型**总结 二、实验结果三、核心…

k8s集群备份与迁移

什么是 Velero? Velero 是一个用Go语言开发的开源工具&#xff0c;用于 Kubernetes 集群的备份、恢复、灾难恢复和迁移。 Velero备份工作流程 当用户发起velero backup create时&#xff0c;会执行如下四个动作&#xff1a; velero客户端调用Kubernetes API创建自定义资源并…

启动windows更新/停止windows更新,在配置更新中关闭自动更新的方法

在Windows操作系统中&#xff0c;启动或停止Windows更新&#xff0c;以及调整“配置更新”的关闭方法&#xff0c;涉及多种途径&#xff0c;这里将详细阐述几种常用的专业方法。 启动Windows更新 1.通过Windows服务管理器&#xff1a; -打开“运行”对话框&#xff08;…

15. 三数之和(实际是双指针类型的题目)

15. 三数之和 15. 三数之和 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以…

Uniapp的alertDialog返回值+async/await处理确定/取消问题

今天在使用uniui的alertDialog时&#xff0c;想添加一个确定/取消的警告框时 发现alertDialog和下面的处理同步进行了&#xff0c;没有等待alaertDialog处理完才进行 查询后发现问题在于 await 关键字虽然被用来等待 alertDialog.value.open() 的完成&#xff0c;但是 alertDi…

Android中的冷启动,热启动和温启动

在App启动方式中分为三种&#xff1a;冷启动&#xff08;cold start&#xff09;、热启动&#xff08;hot start&#xff09;、温启动&#xff08;warm start&#xff09; 冷启动&#xff1a; 系统不存在App进程&#xff08;App首次启动或者App被完全杀死&#xff09;时启动A…

使用 GaLore 预训练LLaMA-7B

项目代码&#xff1a; https://github.com/jiaweizzhao/galorehttps://github.com/jiaweizzhao/galore 参考博客&#xff1a; https://zhuanlan.zhihu.com/p/686686751 创建环境 基础环境配置如下&#xff1a; 操作系统: CentOS 7CPUs: 单个节点具有 1TB 内存的 Intel CP…

F12抓包11:UI自动化 - Recoder(记录器)

课程大纲 使用场景&#xff08;导入和导出&#xff09;: ① 测试的重复性工作&#xff0c;本浏览器录制并进行replay&#xff1b; ② 导入/导出录制脚本&#xff0c;移植后replay&#xff1b; ③ 导出给开发进行replay复现bug&#xff1b; ④ 进行前端性能分析。 1、录制脚…

kubernetes 学习 尚硅谷

出自 https://www.bilibili.com/video/BV13Q4y1C7hS 相关命令 kubeadm init &#xff1a;将当前节点创建为主节点 kubectl get nodes&#xff1a;获取集群所有节点 kubectl apply -f xxx.yaml&#xff1a;根据配置文件&#xff0c;给集群创建资源 kubectl delete -f xx.yaml&…