SpringBoot基于token防止訂單重復(fù)創(chuàng)建
業(yè)務(wù)場景是這樣的
我們在秒殺場景中通常是瘋狂點(diǎn)擊下單
但是最后是只會(huì)創(chuàng)建一個(gè)訂單
點(diǎn)擊一個(gè)下單按鈕是發(fā)一次請求
如何保證一個(gè)用戶一次點(diǎn)擊只創(chuàng)建一個(gè)訂單呢
首先在此之前 我們需要對用戶的權(quán)限進(jìn)行校驗(yàn)
我們這邊使用的token實(shí)現(xiàn)
簽發(fā)token
每次進(jìn)入下單界面 會(huì)簽發(fā)一個(gè)token給瀏覽器 順便寫入redis
然后在下單的時(shí)候 客戶端只要帶這個(gè)token過來
然后順便服務(wù)端校驗(yàn)就行
這個(gè)token使是我們自己簽發(fā)的
我們自己實(shí)現(xiàn)的一個(gè)發(fā)放和存儲(chǔ)
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ā)放一個(gè)基于 UUID 的 token,
* 并將其存儲(chǔ)到 Redis 中。
*
* @author hollis
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("token")
public class TokenController {
/**
* 定義 token 鍵的前綴,用于在 Redis 中存儲(chǔ) token 時(shí)標(biāo)識(shí)鍵。
*/
private static final String TOKEN_PREFIX = "token:";
/**
* 注入 Spring Data Redis 提供的 StringRedisTemplate,
* 用于操作 Redis 中的字符串類型數(shù)據(jù)。
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 該接口用于在用戶登錄狀態(tài)下,根據(jù)傳入的場景信息生成一個(gè)唯一的 token,
* 并將其存儲(chǔ)到 Redis 中,設(shè)置 30 分鐘的過期時(shí)間。
*
* @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()) {
// 生成一個(gè)基于 UUID 的唯一 token
String token = UUID.randomUUID().toString();
// 拼接用于存儲(chǔ)到 Redis 的 token 鍵
String tokenKey = TOKEN_PREFIX + scene + CACHE_KEY_SEPARATOR + token;
// 將 token 存儲(chǔ)到 Redis 中,設(shè)置 30 分鐘的過期時(shí)間
stringRedisTemplate.opsForValue().set(tokenKey, token, 30, TimeUnit.MINUTES);
// 返回包含 token 鍵的成功響應(yīng)
return Result.success(tokenKey);
}
// 若用戶未登錄,拋出用戶未登錄的認(rèn)證異常
throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
}
}我們將這個(gè)token返回個(gè)前端
調(diào)用下單接口是把這個(gè)token帶來
然后去redis里看一下是不是有效就行
有效放過去
無效的話就返回
執(zhí)行其他校驗(yàn)鏈
首先基于 Filter 寫過濾器
在請求過來后首先到達(dá)的是過濾器 然后才是servlet
Filter 是一個(gè)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)過該過濾器時(shí)都會(huì)調(diào)用,用于實(shí)現(xiàn)具體的過濾邏輯
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
// 過濾器銷毀方法,在過濾器實(shí)例銷毀前調(diào)用,用于釋放資源
public default void destroy() {}
}主要有三個(gè)方法
初始化過濾器
過濾方法
過濾器銷毀
我們接下來看這個(gè)方法
具體過濾邏輯 重點(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)境 那么直接生成一個(gè)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 可能會(huì)存在內(nèi)存泄漏的問題
tokenThreadLocal.remove();
stressThreadLocal.remove();
}
}
private boolean checkTokenValidity(String token, Boolean isStress) {
// 獲取指定鍵的值
// 刪除這個(gè)鍵
// 返回獲取到的數(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) {
//如果是壓測,則生成一個(gè)隨機(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ù)一個(gè)ThreadLocal 首先是保證線程安全 其次是在組裝訂單字段的時(shí)候 把token放進(jìn)去做一個(gè)冪等校驗(yàn)
請求進(jìn)來后就到了這邊
首先 通過 mvc提供的 httpRequest從請求頭里面取出token
取出來后我們進(jìn)行校驗(yàn)
調(diào)用checkTokenValidity方法進(jìn)行校驗(yàn)
用LUA腳本去redis里拿這個(gè)token 移除 保證原子性
如果成功了 最后放到ThreadLocal后 繼續(xù)執(zhí)行其他校驗(yàn)鏈
疑問 為什么要基于Filter寫過濾器
使用過濾器能將 Token 校驗(yàn)邏輯集中管理,避免在每個(gè)需要校驗(yàn) Token 的業(yè)務(wù)方法里重復(fù)編寫校驗(yàn)代碼。例如,若有多個(gè)接口都需要進(jìn)行 Token 校驗(yàn),只需配置過濾器攔截這些接口,就能統(tǒng)一進(jìn)行校驗(yàn),而不用在每個(gè)接口方法中重復(fù)寫校驗(yàn)邏輯。

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

