SpringBoot實現(xiàn)接口等冪次校驗的示例代碼
接口等冪性通俗的來說就是同一時間內(nèi),發(fā)起多次請求只有一次請求成功;其目的時防止多次提交,數(shù)據(jù)重復(fù)入庫,表單驗證網(wǎng)絡(luò)延遲重復(fù)提交等問題。
比如:
- 訂單接口, 不能多次創(chuàng)建訂單
- 支付接口, 重復(fù)支付同一筆訂單只能扣一次錢
- 支付寶回調(diào)接口, 可能會多次回調(diào), 必須處理重復(fù)回調(diào)
- 普通表單提交接口, 因為網(wǎng)絡(luò)超時等原因多次點擊提交, 只能成功一次
等等
主流的實現(xiàn)方案如下:
1、唯一索引:給表加唯一索引,該方法最簡單,當(dāng)數(shù)據(jù)重復(fù)插入時,直接報sql異常,對應(yīng)用影響不大;
alter table 表名 add unique(字段)
示例,兩個字段為唯一索引,如果出現(xiàn)完全一樣的order_name,create_time就直接重復(fù)報異常;
alter table 'order' add unique(order_name,create_time)
2、先查詢后判斷:入庫時先查詢是否有該數(shù)據(jù),如果沒有則插入。否則不插入;
3、token機制:發(fā)起請求的時候先去redis獲取token,將獲取的token放入請求的hearder,當(dāng)請求到達服務(wù)端的時候攔截請求,對請求的hearder中的token,進行校驗,如果校驗通過則放開攔截,刪除token,否則使用自定義異常返回錯誤信息。
第一步:書寫redis工具類
? import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisUtils { @Autowired private RedisTemplate<String,Object> redisTemplate; /** * 判斷key是否存在 * @param key 鍵 * @return */ public boolean hasKey(String key){ try { return redisTemplate.hasKey(key); }catch (Exception e){ e.printStackTrace(); return false; } } /** * 刪除key * @param key 鍵 * @return */ public Boolean del(String key){ if (key != null && key.length() > 0){ return redisTemplate.delete(key); }else { return false; } } /** * 普通緩存獲取 * @param key 鍵 * @return */ public Object get(String key){ return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通緩存放入并設(shè)置時間 * @param key 鍵 * @param value 值 * @param time 時間(秒) time要大于0 如果time小于等于0 將設(shè)置無限期 * @return */ public boolean set(String key,Object value,long time){ try { if (time > 0){ redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } return true; }catch (Exception e){ e.printStackTrace(); return false; } } } ?
第二步、書寫token工具類
import com.smile.project.exception.utils.CodeMsg; import com.smile.project.exception.utils.CommonException; import io.netty.util.concurrent.GlobalEventExecutor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * 使用uuid生成隨機字符串, * 通過md5加密防止token被解密,保證token的唯一性與安全性 * 設(shè)置過期時間為30秒,即在30秒內(nèi)之惡能提交成功一次請求 */ @Component public class TokenUtils { @Autowired RedisUtils redisUtils; //token過期時間為30秒 private final static Long TOKEN_EXPIRE = 30L; private final static String TOKEN_NAME = "token"; /** * 生成token放入緩存 */ public String generateToken(){ String uuid = UUID.randomUUID().toString(); String token = DigestUtils.md5DigestAsHex(uuid.getBytes()); redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE); return token; } /** * token校驗 */ public boolean verifyToken(HttpServletRequest request){ String token = request.getHeader(TOKEN_NAME); //header中不存在token if (StringUtils.isEmpty(token)){ //拋出自定義異常 System.out.println("token不存在"); throw new CommonException(CodeMsg.NOT_TOKEN); } //緩存中不存在 if (!redisUtils.hasKey(TOKEN_NAME)){ System.out.println("token已經(jīng)過期"); throw new CommonException(CodeMsg.TIME_OUT_TOKEN); } String cachToken = (String) redisUtils.get(TOKEN_NAME); if (!token.equals(cachToken)){ System.out.println("token檢驗失敗"); throw new CommonException(CodeMsg.VALIDA_ERROR_TOKEN); } //移除token Boolean del = redisUtils.del(TOKEN_NAME); if (!del){ System.out.println("token刪除失敗"); throw new CommonException(CodeMsg.DEL_ERROR_TOKEN); } return true; } }
第三步:定義注解,使用在方法上,當(dāng)控制層的方法上被注釋時,表示該請求為等冪性請求
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 當(dāng)控制層的方法上被注釋時,表示該請求為等冪性請求 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { }
第四步:攔截器配置。選擇前置攔截器,每次請求都校驗到達的方法上是否有等冪性注解,如果有則進行token校驗
import com.smile.project.redis.utils.Idempotent; import com.smile.project.redis.utils.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; ? @Component public class IdempotentInterceptor implements HandlerInterceptor { ? ? ? @Autowired ? ? private TokenUtils tokenUtils; ? ? ? @Override ? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ? ? ? ? if (!(handler instanceof HandlerMethod)){ ? ? ? ? ? ? return true; ? ? ? ? } ? ? ? ? //對有Idempotent注解的方法進行攔截校驗 ? ? ? ? HandlerMethod handlerMethod = (HandlerMethod) handler; ? ? ? ? Method method = handlerMethod.getMethod(); ? ? ? ? Idempotent methodAnnotation = method.getAnnotation(Idempotent.class); ? ? ? ? if (methodAnnotation != null){ ? ? ? ? ? ? //token校驗 ? ? ? ? ? ? tokenUtils.verifyToken(request); ? ? ? ? } ? ? ? ? return true; ? ? } ? ? ? @Override ? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ? ? } ? ? ? @Override ? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ? ? } }
第五步:對攔截器進行url模式匹配,并注入spring容器
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 對攔截器進行url模式匹配,并注入spring容器 */ @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired IdempotentInterceptor idempotentInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //攔截所有請求 registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**"); } }
第六步:控制層
對控制層進行編寫,發(fā)起請求時通過getToken方法獲取token,將獲取的token放入hearder后,再請求具體方法。正常請求具體方法的時候注意在hearder中加入token,否則是失敗
import com.alibaba.fastjson.JSONObject; import com.smile.project.exception.utils.CodeMsg; import com.smile.project.exception.utils.ResultPage; import com.smile.project.redis.utils.Idempotent; import com.smile.project.redis.utils.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SmileController { @Autowired TokenUtils tokenUtils; @GetMapping("smile/token") public ResultPage getToken(){ String token = tokenUtils.generateToken(); JSONObject jsonObject = new JSONObject(); jsonObject.put("token",token); return ResultPage.success(CodeMsg.SUCCESS,jsonObject); } @Idempotent @GetMapping("smile/test") public ResultPage testIdempotent(){ return ResultPage.success(CodeMsg.SUCCESS,"校驗成功"); } }
到此這篇關(guān)于SpringBoot實現(xiàn)接口等冪次校驗的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot實現(xiàn)接口等冪次校驗 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java語言中flush()函數(shù)作用及使用方法詳解
這篇文章主要介紹了Java語言中flush函數(shù)作用及使用方法詳解,具有一定借鑒價值,需要的朋友可以參考下2018-01-01Java傳值調(diào)用和傳引用調(diào)用方式(參數(shù)引用為null問題)
這篇文章主要介紹了Java傳值調(diào)用和傳引用調(diào)用方式(參數(shù)引用為null問題),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09

基于Spring中的事務(wù)@Transactional細節(jié)與易錯點、幻讀

Java中Stream的flatMap與map使用場景及區(qū)別詳解

SpringBoot讀取Resource目錄下文件的四種方式總結(jié)