SpringBoot基于token防止订单重复创建

 更新时间:2025年06月06日 08:30:41   作者:是一只多多  
本文主要介绍了SpringBoot基于token防止订单重复创建,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

业务场景是这样的

我们在秒杀场景中通常是疯狂点击下单

但是最后是只会创建一个订单

点击一个下单按钮是发一次请求

如何保证一个用户一次点击只创建一个订单呢

首先在此之前 我们需要对用户的权限进行校验

我们这边使用的token实现

签发token

每次进入下单界面 会签发一个token给浏览器 顺便写入redis

然后在下单的时候 客户端只要带这个token过来

然后顺便服务端校验就行

这个token使是我们自己签发的

我们自己实现的一个发放和存储

package cn.hollis.nft.turbo.auth.controller;

import cn.dev33.satoken.stp.StpUtil;
import cn.hollis.nft.turbo.auth.exception.AuthErrorCode;
import cn.hollis.nft.turbo.auth.exception.AuthException;
import cn.hollis.nft.turbo.web.vo.Result;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static cn.hollis.nft.turbo.cache.constant.CacheConstant.CACHE_KEY_SEPARATOR;

/**
 * TokenController 类负责处理与 token 相关的请求,
 * 主要功能是在用户登录状态下生成并发放一个基于 UUID 的 token,
 * 并将其存储到 Redis 中。
 *
 * @author hollis
 */
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("token")
public class TokenController {

    /**
     * 定义 token 键的前缀,用于在 Redis 中存储 token 时标识键。
     */
    private static final String TOKEN_PREFIX = "token:";

    /**
     * 注入 Spring Data Redis 提供的 StringRedisTemplate,
     * 用于操作 Redis 中的字符串类型数据。
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 该接口用于在用户登录状态下,根据传入的场景信息生成一个唯一的 token,
     * 并将其存储到 Redis 中,设置 30 分钟的过期时间。
     *
     * @param scene 生成 token 的场景信息,不能为空。
     * @return 封装了生成的 token 键的统一响应对象 Result。
     * @throws AuthException 若用户未登录,抛出用户未登录的认证异常。
     */
    @GetMapping("/get")
    public Result<String> get(@NotBlank String scene) {
        // 检查用户是否已登录
        if (StpUtil.isLogin()) {
            // 生成一个基于 UUID 的唯一 token
            String token = UUID.randomUUID().toString();
            // 拼接用于存储到 Redis 的 token 键
            String tokenKey = TOKEN_PREFIX + scene + CACHE_KEY_SEPARATOR + token;
            // 将 token 存储到 Redis 中,设置 30 分钟的过期时间
            stringRedisTemplate.opsForValue().set(tokenKey, token, 30, TimeUnit.MINUTES);
            // 返回包含 token 键的成功响应
            return Result.success(tokenKey);
        }
        // 若用户未登录,抛出用户未登录的认证异常
        throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
    }
}

我们将这个token返回个前端

调用下单接口是把这个token带来

然后去redis里看一下是不是有效就行

有效放过去

无效的话就返回

执行其他校验链

首先基于 Filter 写过滤器

在请求过来后首先到达的是过滤器 然后才是servlet

Filter 是一个servlet组件

package jakarta.servlet;

import java.io.IOException;

public interface Filter {
    // 过滤器初始化方法,在过滤器实例创建后调用,用于初始化资源
    public default void init(FilterConfig filterConfig) throws ServletException {}

    // 过滤方法,每次请求经过该过滤器时都会调用,用于实现具体的过滤逻辑
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    // 过滤器销毁方法,在过滤器实例销毁前调用,用于释放资源
    public default void destroy() {}
}

主要有三个方法

初始化过滤器

过滤方法

过滤器销毁

我们接下来看这个方法

具体过滤逻辑 重点

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

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.BooleanUtils;
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;
import java.util.UUID;

/**
 * @author Hollis
 */
public class TokenFilter implements Filter {

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

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

    public static final ThreadLocal<Boolean> stressThreadLocal = new ThreadLocal<>();

    private RedissonClient redissonClient;

    // 选择构造器注入bean的方式 是spring官方推荐的注入方式
    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");
            // 原来的逻辑是从redis里获取并且验证token
            // 如果是压测环境 那么直接生成一个UUID作为Token
            Boolean isStress = BooleanUtils.toBoolean(httpRequest.getHeader("isStress"));

            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, isStress);
            // 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 {
            // ThreadLocal 可能会存在内存泄漏的问题
            tokenThreadLocal.remove();
            stressThreadLocal.remove();
        }
    }

    private boolean checkTokenValidity(String token, Boolean isStress) {
        // 获取指定键的值
        // 删除这个键
        // 返回获取到的数值
        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);
        String result = (String) redissonClient.getScript().eval(RScript.Mode.READ_WRITE,
                luaScript,
                RScript.ReturnType.STATUS,
                Arrays.asList(token));
        if (isStress) {
            //如果是压测,则生成一个随机数,模拟 token
            result = UUID.randomUUID().toString();
            stressThreadLocal.set(isStress);
        }
        tokenThreadLocal.set(result);
        return result != null;

    }

    @Override
    public void destroy() {
        // 过滤器销毁,可选实现
    }
}

注入的是 redissonClient 即redis的客户端

同样我们要指定log 用于打印日志

还维护一个ThreadLocal 首先是保证线程安全 其次是在组装订单字段的时候 把token放进去做一个幂等校验

请求进来后就到了这边

首先 通过 mvc提供的 httpRequest从请求头里面取出token
取出来后我们进行校验

调用checkTokenValidity方法进行校验

用LUA脚本去redis里拿这个token 移除 保证原子性

如果成功了 最后放到ThreadLocal后 继续执行其他校验链

疑问 为什么要基于Filter写过滤器

使用过滤器能将 Token 校验逻辑集中管理,避免在每个需要校验 Token 的业务方法里重复编写校验代码。例如,若有多个接口都需要进行 Token 校验,只需配置过滤器拦截这些接口,就能统一进行校验,而不用在每个接口方法中重复写校验逻辑。

接着是在 Spring MVC 处入口配置

只有先配置了 过滤器才能生效

我们是在这里添加token的校验 URL配置等

可以理解成注册 filter 过滤器

"注册" 在这段代码中有两层含义:

一. 是把对象注册到 Spring 容器进行管理;

二. 是将 Servlet 过滤器注册到 Servlet 容器,使其能在请求处理流程中发挥作用。

通过 FilterRegistrationBean,将 TokenFilter 过滤器注册到 Servlet 容器中,Servlet 容器会在处理请求时,按照配置的规则调用 TokenFilter 进行过滤操作。

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

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;

/**
 * @author Hollis
 * 这是所有mvc入口的一个配置 实现了 WebMvcConfigurer接口
 * AutoConfiguration注解 标记此类为自动配置类
 * ConditionalOnWebApplication 条件注释 表示在web环境下 配置类生效
 * 注册了一系列过滤器
 */
@AutoConfiguration
@ConditionalOnWebApplication
public class WebConfiguration implements WebMvcConfigurer {

    @Bean
    @ConditionalOnMissingBean
    GlobalWebExceptionHandler globalWebExceptionHandler() {
        return new GlobalWebExceptionHandler();
    }

    /**
     * 注册token过滤器
     *
     * @param redissonClient
     * @return
     */
    @Bean
    public FilterRegistrationBean<TokenFilter> tokenFilter(RedissonClient redissonClient) {
        FilterRegistrationBean<TokenFilter> registrationBean = new FilterRegistrationBean<>();
        // 设置要注册的过滤器为 TokenFilter 实例,并传入 RedissonClient 对象。
        registrationBean.setFilter(new TokenFilter(redissonClient));
        // 设置过滤器需要拦截的 URL 路径,只有请求路径匹配这些模式时,TokenFilter 才会处理该请求。
        registrationBean.addUrlPatterns("/trade/buy","/trade/newBuy","/trade/normalBuy");
        // 设置过滤器的执行顺序,数字越小,执行优先级越高。
        registrationBean.setOrder(10);

        return registrationBean;
    }

}

到此这篇关于SpringBoot基于token防止订单重复创建的文章就介绍到这了,更多相关SpringBoot token防止订单重复内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Java TCP网络通信协议详细讲解

    Java TCP网络通信协议详细讲解

    TCP/IP是一种面向连接的、可靠的、基于字节流的传输层通信协议,它会保证数据不丢包、不乱序。TCP全名是Transmission Control Protocol,它是位于网络OSI模型中的第四层
    2022-09-09
  • Java OpenCV4.0.0实现实时人脸识别

    Java OpenCV4.0.0实现实时人脸识别

    这篇文章主要为大家详细介绍了Java OpenCV4.0.0实现实时人脸识别,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-07-07
  • java8中的HashMap原理详解

    java8中的HashMap原理详解

    这篇文章主要介绍了java8中的HashMap原理详解,HashMap是日常开发中非常常用的容器,HashMap实现了Map接口,底层的实现原理是哈希表,HashMap不是一个线程安全的容器,需要的朋友可以参考下
    2023-09-09
  • Java Maven构建工具中mvnd和Gradle谁更快

    Java Maven构建工具中mvnd和Gradle谁更快

    这篇文章主要介绍了Java Maven构建工具中mvnd和Gradle谁更快,mvnd 是 Maven Daemon 的缩写 ,翻译成中文就是 Maven 守护进程,下文更多相关资料,需要的小伙伴可以参考一下
    2022-05-05
  • idea编写yml、yaml文件以及其优先级的使用

    idea编写yml、yaml文件以及其优先级的使用

    本文主要介绍了idea编写yml、yaml文件以及其优先级的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • IDEA部署JavaWeb项目到Tomcat服务器的方法

    IDEA部署JavaWeb项目到Tomcat服务器的方法

    这篇文章主要介绍了IDEA部署JavaWeb项目到Tomcat服务器的方法,本文给大家介绍的非常详细,感兴趣的朋友跟随脚本之家小编一起学习吧
    2018-06-06
  • Java中String类的一些常见方法总结

    Java中String类的一些常见方法总结

    这篇文章主要给大家介绍了关于Java中String类的一些常见方法,文中包括了Java中String类的基本概念、构造方式、常用方法以及StringBuilder和StringBuffer的使用,涵盖了字符串操作的各个方面,包括查找、转换、比较、替换、拆分、截取等,需要的朋友可以参考下
    2024-11-11
  • 浅谈Java中的private方法是否可以被代理

    浅谈Java中的private方法是否可以被代理

    这篇文章主要介绍了浅谈Java中的private方法是否可以被代理,在 Java 8之前,接口可以有常量变量和抽象方法,我们不能在接口中提供方法实现,如果我们要提供抽象方法和非抽象方法(方法与实现)的组合,那么我们就得使用抽象类,需要的朋友可以参考下
    2023-12-12
  • SimpleDateFormat线程安全问题排查详解

    SimpleDateFormat线程安全问题排查详解

    这篇文章主要为大家介绍了SimpleDateFormat线程安全问题排查详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • Spring中Bean的命名方式代码详解

    Spring中Bean的命名方式代码详解

    这篇文章主要介绍了Spring中Bean的命名方式代码详解,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01

最新评论