SpringBoot基于token防止訂單重復(fù)創(chuàng)建
業(yè)務(wù)場(chǎng)景是這樣的
我們?cè)诿霘?chǎng)景中通常是瘋狂點(diǎn)擊下單
但是最后是只會(huì)創(chuàng)建一個(gè)訂單
點(diǎn)擊一個(gè)下單按鈕是發(fā)一次請(qǐng)求
如何保證一個(gè)用戶一次點(diǎn)擊只創(chuàng)建一個(gè)訂單呢
首先在此之前 我們需要對(duì)用戶的權(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)的請(qǐng)求, * 主要功能是在用戶登錄狀態(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ù)傳入的場(chǎng)景信息生成一個(gè)唯一的 token, * 并將其存儲(chǔ)到 Redis 中,設(shè)置 30 分鐘的過期時(shí)間。 * * @param scene 生成 token 的場(chǎng)景信息,不能為空。 * @return 封裝了生成的 token 鍵的統(tǒng)一響應(yīng)對(duì)象 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 寫過濾器
在請(qǐng)求過來后首先到達(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 {} // 過濾方法,每次請(qǐng)求經(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; // 從請(qǐng)求頭中獲取Token String token = httpRequest.getHeader("Authorization"); // 原來的邏輯是從redis里獲取并且驗(yàn)證token // 如果是壓測(cè)環(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) { //如果是壓測(cè),則生成一個(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)
請(qǐng)求進(jìn)來后就到了這邊
首先 通過 mvc提供的 httpRequest從請(qǐng)求頭里面取出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配置等
可以理解成注冊(cè) filter 過濾器
"注冊(cè)" 在這段代碼中有兩層含義:
一. 是把對(duì)象注冊(cè)到 Spring 容器進(jìn)行管理;
二. 是將 Servlet 過濾器注冊(cè)到 Servlet 容器,使其能在請(qǐng)求處理流程中發(fā)揮作用。
通過 FilterRegistrationBean
,將 TokenFilter
過濾器注冊(cè)到 Servlet 容器中,Servlet 容器會(huì)在處理請(qǐng)求時(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)境下 配置類生效 * 注冊(cè)了一系列過濾器 */ @AutoConfiguration @ConditionalOnWebApplication public class WebConfiguration implements WebMvcConfigurer { @Bean @ConditionalOnMissingBean GlobalWebExceptionHandler globalWebExceptionHandler() { return new GlobalWebExceptionHandler(); } /** * 注冊(cè)token過濾器 * * @param redissonClient * @return */ @Bean public FilterRegistrationBean<TokenFilter> tokenFilter(RedissonClient redissonClient) { FilterRegistrationBean<TokenFilter> registrationBean = new FilterRegistrationBean<>(); // 設(shè)置要注冊(cè)的過濾器為 TokenFilter 實(shí)例,并傳入 RedissonClient 對(duì)象。 registrationBean.setFilter(new TokenFilter(redissonClient)); // 設(shè)置過濾器需要攔截的 URL 路徑,只有請(qǐng)求路徑匹配這些模式時(shí),TokenFilter 才會(huì)處理該請(qǐng)求。 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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis使用typeHandler加密的實(shí)現(xiàn)
本文詳細(xì)介紹了如何在Mybatis中使用typeHandler對(duì)特定字段進(jìn)行加密處理,涵蓋了從引入依賴、配置Mybatis,到實(shí)現(xiàn)typeHandler繼承類和配置mapper層的詳細(xì)步驟,為需要在項(xiàng)目中實(shí)現(xiàn)字段加密的開發(fā)者提供了參考和借鑒2024-09-09MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤
這篇文章主要介紹了MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08IDEA中將SpringBoot項(xiàng)目提交到git倉庫的方法步驟
本文主要介紹了IDEA中將SpringBoot項(xiàng)目提交到git倉庫的方法步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12SpringBoot項(xiàng)目配置postgresql數(shù)據(jù)庫完整步驟(配置多數(shù)據(jù)源)
PostgreSQL是一種特性非常齊全的自由軟件的對(duì)象-關(guān)系型數(shù)據(jù)庫管理系統(tǒng)(ORDBMS),下面這篇文章主要給大家介紹了關(guān)于SpringBoot項(xiàng)目配置postgresql數(shù)據(jù)庫(配置多數(shù)據(jù)源)的相關(guān)資料,需要的朋友可以參考下2023-05-05java中Servlet監(jiān)聽器的工作原理及示例詳解
這篇文章主要介紹了java中Servlet監(jiān)聽器的工作原理及示例詳解。Servlet監(jiān)聽器用于監(jiān)聽一些重要事件的發(fā)生,監(jiān)聽器對(duì)象可以在事情發(fā)生前、發(fā)生后可以做一些必要的處理。感興趣的可以來了解一下2020-07-07SpringBoot WebService服務(wù)端&客戶端使用案例教程
這篇文章主要介紹了SpringBoot WebService服務(wù)端&客戶端使用案例教程,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-10-10