最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot基于token防止訂單重復(fù)創(chuàng)建

 更新時間:2025年06月06日 08:30:41   作者:是一只多多  
本文主要介紹了SpringBoot基于token防止訂單重復(fù)創(chuàng)建,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

業(yè)務(wù)場景是這樣的

我們在秒殺場景中通常是瘋狂點(diǎn)擊下單

但是最后是只會創(chuàng)建一個訂單

點(diǎn)擊一個下單按鈕是發(fā)一次請求

如何保證一個用戶一次點(diǎn)擊只創(chuàng)建一個訂單呢

首先在此之前 我們需要對用戶的權(quán)限進(jìn)行校驗(yàn)

我們這邊使用的token實(shí)現(xiàn)

簽發(fā)token

每次進(jìn)入下單界面 會簽發(fā)一個token給瀏覽器 順便寫入redis

然后在下單的時候 客戶端只要帶這個token過來

然后順便服務(wù)端校驗(yàn)就行

這個token使是我們自己簽發(fā)的

我們自己實(shí)現(xiàn)的一個發(fā)放和存儲

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 類負(fù)責(zé)處理與 token 相關(guān)的請求,
 * 主要功能是在用戶登錄狀態(tài)下生成并發(fā)放一個基于 UUID 的 token,
 * 并將其存儲到 Redis 中。
 *
 * @author hollis
 */
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("token")
public class TokenController {

    /**
     * 定義 token 鍵的前綴,用于在 Redis 中存儲 token 時標(biāo)識鍵。
     */
    private static final String TOKEN_PREFIX = "token:";

    /**
     * 注入 Spring Data Redis 提供的 StringRedisTemplate,
     * 用于操作 Redis 中的字符串類型數(shù)據(jù)。
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 該接口用于在用戶登錄狀態(tài)下,根據(jù)傳入的場景信息生成一個唯一的 token,
     * 并將其存儲到 Redis 中,設(shè)置 30 分鐘的過期時間。
     *
     * @param scene 生成 token 的場景信息,不能為空。
     * @return 封裝了生成的 token 鍵的統(tǒng)一響應(yīng)對象 Result。
     * @throws AuthException 若用戶未登錄,拋出用戶未登錄的認(rèn)證異常。
     */
    @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 中,設(shè)置 30 分鐘的過期時間
            stringRedisTemplate.opsForValue().set(tokenKey, token, 30, TimeUnit.MINUTES);
            // 返回包含 token 鍵的成功響應(yīng)
            return Result.success(tokenKey);
        }
        // 若用戶未登錄,拋出用戶未登錄的認(rèn)證異常
        throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
    }
}

我們將這個token返回個前端

調(diào)用下單接口是把這個token帶來

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

有效放過去

無效的話就返回

執(zhí)行其他校驗(yàn)鏈

首先基于 Filter 寫過濾器

在請求過來后首先到達(dá)的是過濾器 然后才是servlet

Filter 是一個servlet組件

package jakarta.servlet;

import java.io.IOException;

public interface Filter {
    // 過濾器初始化方法,在過濾器實(shí)例創(chuàng)建后調(diào)用,用于初始化資源
    public default void init(FilterConfig filterConfig) throws ServletException {}

    // 過濾方法,每次請求經(jīng)過該過濾器時都會調(diào)用,用于實(shí)現(xiàn)具體的過濾邏輯
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    // 過濾器銷毀方法,在過濾器實(shí)例銷毀前調(diào)用,用于釋放資源
    public default void destroy() {}
}

主要有三個方法

初始化過濾器

過濾方法

過濾器銷毀

我們接下來看這個方法

具體過濾邏輯 重點(diǎn)

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;

    // 選擇構(gòu)造器注入bean的方式 是spring官方推薦的注入方式
    public TokenFilter(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 過濾器初始化,可選實(shí)現(xiàn)
    }

    @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里獲取并且驗(yàn)證token
            // 如果是壓測環(huán)境 那么直接生成一個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;
            }
            // 校驗(yàn)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有效,繼續(xù)執(zhí)行其他過濾器鏈
            chain.doFilter(request, response);
        } finally {
            // ThreadLocal 可能會存在內(nèi)存泄漏的問題
            tokenThreadLocal.remove();
            stressThreadLocal.remove();
        }
    }

    private boolean checkTokenValidity(String token, Boolean isStress) {
        // 獲取指定鍵的值
        // 刪除這個鍵
        // 返回獲取到的數(shù)值
        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) {
            //如果是壓測,則生成一個隨機(jī)數(shù),模擬 token
            result = UUID.randomUUID().toString();
            stressThreadLocal.set(isStress);
        }
        tokenThreadLocal.set(result);
        return result != null;

    }

    @Override
    public void destroy() {
        // 過濾器銷毀,可選實(shí)現(xiàn)
    }
}

注入的是 redissonClient 即redis的客戶端

同樣我們要指定log 用于打印日志

還維護(hù)一個ThreadLocal 首先是保證線程安全 其次是在組裝訂單字段的時候 把token放進(jìn)去做一個冪等校驗(yàn)

請求進(jìn)來后就到了這邊

首先 通過 mvc提供的 httpRequest從請求頭里面取出token
取出來后我們進(jìn)行校驗(yàn)

調(diào)用checkTokenValidity方法進(jìn)行校驗(yàn)

用LUA腳本去redis里拿這個token 移除 保證原子性

如果成功了 最后放到ThreadLocal后 繼續(xù)執(zhí)行其他校驗(yàn)鏈

疑問 為什么要基于Filter寫過濾器

使用過濾器能將 Token 校驗(yàn)邏輯集中管理,避免在每個需要校驗(yàn) Token 的業(yè)務(wù)方法里重復(fù)編寫校驗(yàn)代碼。例如,若有多個接口都需要進(jìn)行 Token 校驗(yàn),只需配置過濾器攔截這些接口,就能統(tǒng)一進(jìn)行校驗(yàn),而不用在每個接口方法中重復(fù)寫校驗(yàn)邏輯。

接著是在 Spring MVC 處入口配置

只有先配置了 過濾器才能生效

我們是在這里添加token的校驗(yàn) URL配置等

可以理解成注冊 filter 過濾器

"注冊" 在這段代碼中有兩層含義:

一. 是把對象注冊到 Spring 容器進(jìn)行管理;

二. 是將 Servlet 過濾器注冊到 Servlet 容器,使其能在請求處理流程中發(fā)揮作用。

通過 FilterRegistrationBean,將 TokenFilter 過濾器注冊到 Servlet 容器中,Servlet 容器會在處理請求時,按照配置的規(guī)則調(diào)用 TokenFilter 進(jìn)行過濾操作。

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入口的一個配置 實(shí)現(xiàn)了 WebMvcConfigurer接口
 * AutoConfiguration注解 標(biāo)記此類為自動配置類
 * ConditionalOnWebApplication 條件注釋 表示在web環(huán)境下 配置類生效
 * 注冊了一系列過濾器
 */
@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<>();
        // 設(shè)置要注冊的過濾器為 TokenFilter 實(shí)例,并傳入 RedissonClient 對象。
        registrationBean.setFilter(new TokenFilter(redissonClient));
        // 設(shè)置過濾器需要攔截的 URL 路徑,只有請求路徑匹配這些模式時,TokenFilter 才會處理該請求。
        registrationBean.addUrlPatterns("/trade/buy","/trade/newBuy","/trade/normalBuy");
        // 設(shè)置過濾器的執(zhí)行順序,數(shù)字越小,執(zhí)行優(yōu)先級越高。
        registrationBean.setOrder(10);

        return registrationBean;
    }

}

到此這篇關(guān)于SpringBoot基于token防止訂單重復(fù)創(chuàng)建的文章就介紹到這了,更多相關(guān)SpringBoot token防止訂單重復(fù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家! 

相關(guān)文章

  • Java TCP網(wǎng)絡(luò)通信協(xié)議詳細(xì)講解

    Java TCP網(wǎng)絡(luò)通信協(xié)議詳細(xì)講解

    TCP/IP是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議,它會保證數(shù)據(jù)不丟包、不亂序。TCP全名是Transmission?Control?Protocol,它是位于網(wǎng)絡(luò)OSI模型中的第四層
    2022-09-09
  • Java OpenCV4.0.0實(shí)現(xiàn)實(shí)時人臉識別

    Java OpenCV4.0.0實(shí)現(xiàn)實(shí)時人臉識別

    這篇文章主要為大家詳細(xì)介紹了Java OpenCV4.0.0實(shí)現(xiàn)實(shí)時人臉識別,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • java8中的HashMap原理詳解

    java8中的HashMap原理詳解

    這篇文章主要介紹了java8中的HashMap原理詳解,HashMap是日常開發(fā)中非常常用的容器,HashMap實(shí)現(xiàn)了Map接口,底層的實(shí)現(xiàn)原理是哈希表,HashMap不是一個線程安全的容器,需要的朋友可以參考下
    2023-09-09
  • Java?Maven構(gòu)建工具中mvnd和Gradle誰更快

    Java?Maven構(gòu)建工具中mvnd和Gradle誰更快

    這篇文章主要介紹了Java?Maven構(gòu)建工具中mvnd和Gradle誰更快,mvnd?是?Maven?Daemon?的縮寫?,翻譯成中文就是?Maven?守護(hù)進(jìn)程,下文更多相關(guān)資料,需要的小伙伴可以參考一下
    2022-05-05
  • idea編寫yml、yaml文件以及其優(yōu)先級的使用

    idea編寫yml、yaml文件以及其優(yōu)先級的使用

    本文主要介紹了idea編寫yml、yaml文件以及其優(yōu)先級的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • IDEA部署JavaWeb項(xiàng)目到Tomcat服務(wù)器的方法

    IDEA部署JavaWeb項(xiàng)目到Tomcat服務(wù)器的方法

    這篇文章主要介紹了IDEA部署JavaWeb項(xiàng)目到Tomcat服務(wù)器的方法,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧
    2018-06-06
  • Java中String類的一些常見方法總結(jié)

    Java中String類的一些常見方法總結(jié)

    這篇文章主要給大家介紹了關(guān)于Java中String類的一些常見方法,文中包括了Java中String類的基本概念、構(gòu)造方式、常用方法以及StringBuilder和StringBuffer的使用,涵蓋了字符串操作的各個方面,包括查找、轉(zhuǎn)換、比較、替換、拆分、截取等,需要的朋友可以參考下
    2024-11-11
  • 淺談Java中的private方法是否可以被代理

    淺談Java中的private方法是否可以被代理

    這篇文章主要介紹了淺談Java中的private方法是否可以被代理,在?Java?8之前,接口可以有常量變量和抽象方法,我們不能在接口中提供方法實(shí)現(xiàn),如果我們要提供抽象方法和非抽象方法(方法與實(shí)現(xiàn))的組合,那么我們就得使用抽象類,需要的朋友可以參考下
    2023-12-12
  • SimpleDateFormat線程安全問題排查詳解

    SimpleDateFormat線程安全問題排查詳解

    這篇文章主要為大家介紹了SimpleDateFormat線程安全問題排查詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • Spring中Bean的命名方式代碼詳解

    Spring中Bean的命名方式代碼詳解

    這篇文章主要介紹了Spring中Bean的命名方式代碼詳解,具有一定借鑒價值,需要的朋友可以參考下
    2018-01-01

最新評論

桂平市| 潍坊市| 若尔盖县| 庆安县| 华容县| 湘西| 灯塔市| 合水县| 喀喇沁旗| 黎城县| 稷山县| 万源市| 泗阳县| 多伦县| 简阳市| 齐河县| 张北县| 宜兰市| 新平| 井陉县| 区。| 乐安县| 南城县| 澄城县| 临邑县| 军事| 洛川县| 延吉市| 云阳县| 连平县| 芜湖县| 女性| SHOW| 龙门县| 承德市| 正安县| 恭城| 闵行区| 垫江县| 铁岭县| 凤庆县|