SpringBoot基于token防止訂單重復(fù)創(chuàng)建
業(yè)務(wù)場景是這樣的
我們在秒殺場景中通常是瘋狂點擊下單
但是最后是只會創(chuàng)建一個訂單
點擊一個下單按鈕是發(fā)一次請求
如何保證一個用戶一次點擊只創(chuàng)建一個訂單呢
首先在此之前 我們需要對用戶的權(quán)限進行校驗
我們這邊使用的token實現(xiàn)
簽發(fā)token
每次進入下單界面 會簽發(fā)一個token給瀏覽器 順便寫入redis
然后在下單的時候 客戶端只要帶這個token過來
然后順便服務(wù)端校驗就行
這個token使是我們自己簽發(fā)的
我們自己實現(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 類負責(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 若用戶未登錄,拋出用戶未登錄的認證異常。
*/
@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);
}
// 若用戶未登錄,拋出用戶未登錄的認證異常
throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
}
}我們將這個token返回個前端
調(diào)用下單接口是把這個token帶來
然后去redis里看一下是不是有效就行
有效放過去
無效的話就返回
執(zhí)行其他校驗鏈
首先基于 Filter 寫過濾器
在請求過來后首先到達的是過濾器 然后才是servlet
Filter 是一個servlet組件
package jakarta.servlet;
import java.io.IOException;
public interface Filter {
// 過濾器初始化方法,在過濾器實例創(chuàng)建后調(diào)用,用于初始化資源
public default void init(FilterConfig filterConfig) throws ServletException {}
// 過濾方法,每次請求經(jīng)過該過濾器時都會調(diào)用,用于實現(xiàn)具體的過濾邏輯
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
// 過濾器銷毀方法,在過濾器實例銷毀前調(diào)用,用于釋放資源
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;
// 選擇構(gòu)造器注入bean的方式 是spring官方推薦的注入方式
public TokenFilter(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 過濾器初始化,可選實現(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里獲取并且驗證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;
}
// 校驗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) {
//如果是壓測,則生成一個隨機數(shù),模擬 token
result = UUID.randomUUID().toString();
stressThreadLocal.set(isStress);
}
tokenThreadLocal.set(result);
return result != null;
}
@Override
public void destroy() {
// 過濾器銷毀,可選實現(xiàn)
}
}
注入的是 redissonClient 即redis的客戶端
同樣我們要指定log 用于打印日志
還維護一個ThreadLocal 首先是保證線程安全 其次是在組裝訂單字段的時候 把token放進去做一個冪等校驗
請求進來后就到了這邊
首先 通過 mvc提供的 httpRequest從請求頭里面取出token
取出來后我們進行校驗
調(diào)用checkTokenValidity方法進行校驗
用LUA腳本去redis里拿這個token 移除 保證原子性
如果成功了 最后放到ThreadLocal后 繼續(xù)執(zhí)行其他校驗鏈
疑問 為什么要基于Filter寫過濾器
使用過濾器能將 Token 校驗邏輯集中管理,避免在每個需要校驗 Token 的業(yè)務(wù)方法里重復(fù)編寫校驗代碼。例如,若有多個接口都需要進行 Token 校驗,只需配置過濾器攔截這些接口,就能統(tǒng)一進行校驗,而不用在每個接口方法中重復(fù)寫校驗邏輯。

接著是在 Spring MVC 處入口配置
只有先配置了 過濾器才能生效
我們是在這里添加token的校驗 URL配置等
可以理解成注冊 filter 過濾器
"注冊" 在這段代碼中有兩層含義:
一. 是把對象注冊到 Spring 容器進行管理;
二. 是將 Servlet 過濾器注冊到 Servlet 容器,使其能在請求處理流程中發(fā)揮作用。
通過 FilterRegistrationBean,將 TokenFilter 過濾器注冊到 Servlet 容器中,Servlet 容器會在處理請求時,按照配置的規(guī)則調(diào)用 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入口的一個配置 實現(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 實例,并傳入 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)文章
Mybatis使用typeHandler加密的實現(xiàn)
本文詳細介紹了如何在Mybatis中使用typeHandler對特定字段進行加密處理,涵蓋了從引入依賴、配置Mybatis,到實現(xiàn)typeHandler繼承類和配置mapper層的詳細步驟,為需要在項目中實現(xiàn)字段加密的開發(fā)者提供了參考和借鑒2024-09-09
MyBatis版本升級導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤
這篇文章主要介紹了MyBatis版本升級導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
IDEA中將SpringBoot項目提交到git倉庫的方法步驟
本文主要介紹了IDEA中將SpringBoot項目提交到git倉庫的方法步驟,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12
SpringBoot項目配置postgresql數(shù)據(jù)庫完整步驟(配置多數(shù)據(jù)源)
PostgreSQL是一種特性非常齊全的自由軟件的對象-關(guān)系型數(shù)據(jù)庫管理系統(tǒng)(ORDBMS),下面這篇文章主要給大家介紹了關(guān)于SpringBoot項目配置postgresql數(shù)據(jù)庫(配置多數(shù)據(jù)源)的相關(guān)資料,需要的朋友可以參考下2023-05-05
java中Servlet監(jiān)聽器的工作原理及示例詳解
這篇文章主要介紹了java中Servlet監(jiān)聽器的工作原理及示例詳解。Servlet監(jiān)聽器用于監(jiān)聽一些重要事件的發(fā)生,監(jiān)聽器對象可以在事情發(fā)生前、發(fā)生后可以做一些必要的處理。感興趣的可以來了解一下2020-07-07
SpringBoot WebService服務(wù)端&客戶端使用案例教程
這篇文章主要介紹了SpringBoot WebService服務(wù)端&客戶端使用案例教程,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-10-10

