- 保證接口冪等性,表單重復提交
前台解決方案:
提交后按鈕禁用、置灰、頁面出現遮罩
后台解決方案: 使用token,每個token只能使用一次
1.在調用接口之前生成對應的Token,存放至redis
2.在調用接口時,將生成的令牌放入請求request中
3.接口提交的時候獲取對應的令牌,如果能夠從redis中獲得該令牌(獲取后將當前令牌刪除),
則繼續執行訪問的業務邏輯
4.接口提交的時候獲取對應的令牌,如果獲取不到改令牌,則直接返回請勿提交
工程源碼:https://github.com/youxiu326/sb_more_submit
自定義注解
ApiToken注解用於將token保存至request,用於頁面取token
ApiRepeatSubmit注解用於標明改方法需要驗證token才能提交

package com.huarui.util; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 生成token注解 */ @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiToken { }

package com.huarui.util; import com.huarui.common.ConstantUtils; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @功能描述 防止重復提交標記注解 */ @Target(ElementType.METHOD) // 作用到方法上 @Retention(RetentionPolicy.RUNTIME)// 運行時有效 public @interface ApiRepeatSubmit { ConstantUtils value(); }

package com.huarui.common; /** * 【定義從哪里取Token的枚舉類】 * head 即從請求頭中取token,即客戶端將token放入請求頭來請求后端數據 * body 即直接從請求體中取token */ public enum ConstantUtils { BOOD,HEAD }

spring.thymeleaf.cache=false spring.redis.host=youxiu326.xin spring.redis.port=6379

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.19.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.huarui</groupId> <artifactId>sb_more_submit</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sb_more_submit</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version> <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
切面攔截,

package com.huarui.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * redis工具類 */ @Component public class RedisTokenUtils { private long timeout = 2;//過期時間 @Autowired private RedisTemplate redisTemplate; /** * 獲取Token 並將Token保存至redis * @return */ public String getToken() { String token = "token_"+ UUID.randomUUID(); redisTemplate.opsForValue().set(token,token,timeout, TimeUnit.MINUTES); return token; } /** * 判斷Token是否存在 並且刪除Token * @param tokenKey * @return */ public boolean findToken(String tokenKey){ String token = (String) redisTemplate.opsForValue().get(tokenKey); if (StringUtils.isEmpty(token)) { return false; } // token 獲取成功后 刪除對應tokenMapstoken redisTemplate.delete(tokenKey); return true; } }
package com.huarui.aop; import javax.servlet.http.HttpServletRequest; import com.huarui.common.ConstantUtils; import com.huarui.util.ApiToken; import com.huarui.util.ApiRepeatSubmit; import com.huarui.util.RedisTokenUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.concurrent.TimeUnit; /** * @功能描述 aop解析注解 */ @Aspect @Component public class NoRepeatSubmitAop { private Log logger = LogFactory.getLog(getClass()); @Autowired private RedisTokenUtils redisTokenUtils; /** * 將token放入請求 * @param pjp * @param nrs */ @Before("execution(* com.huarui.controller.*Controller.*(..)) && @annotation(nrs)") public void before(JoinPoint pjp, ApiToken nrs){ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); request.setAttribute("token", redisTokenUtils.getToken()); } /** * 攔截帶有重復請求的注解的方法 * @param pjp * @param nrs * @return */ @Around("execution(* com.huarui.controller.*Controller.*(..)) && @annotation(nrs)") public Object arround(ProceedingJoinPoint pjp, ApiRepeatSubmit nrs) { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String token = null; if (nrs.value() == ConstantUtils.BOOD){ //從請求體中取Token token = (String) request.getAttribute("token"); }else if (nrs.value() == ConstantUtils.HEAD){ //從請求頭中取Token token = request.getHeader("token"); } if (StringUtils.isEmpty(token)){ return "token 不存在"; } if (!redisTokenUtils.findToken(token)){ return "請勿重復提交"; } Object o = pjp.proceed(); return o; } catch (Throwable e) { e.printStackTrace(); logger.error("驗證重復提交時出現未知異常!"); return "{\"code\":-889,\"message\":\"驗證重復提交時出現未知異常!\"}"; } } }
package com.huarui.controller; import com.huarui.common.ConstantUtils; import com.huarui.util.ApiRepeatSubmit; import com.huarui.util.ApiToken; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; @Controller public class TestController { /** * 進入頁面 * @return */ @GetMapping("/") @ApiToken public String index(){ return "index"; } /** * 測試重復提交接口 * 將Token放入請求頭中 * @return */ @RequestMapping("/test") @ApiRepeatSubmit(ConstantUtils.HEAD) public @ResponseBody String test() { return ("程序邏輯返回"); } }
前端頁面:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <base th:href="${#httpServletRequest.getContextPath()+'/'}"> <meta charset="UTF-8"> <title>測試表單重復功能</title> </head> <body> <td colspan="1"><button type="button" onclick="add()">加購</button></td> </body> <script src="/jquery-1.11.3.min.js"></script> <script th:inline="javascript"> function add(){ //取得token參數 var token = [[${token}]]; console.log("獲取到的token:" + token); $.ajax({ type: 'POST', url: "/test", data: {}, headers: { "token":token, }, // dataType: "json", success: function(response){ alert(response); }, error:function(response){ alert(response); console.log(response); } }); } </script> </html>
工程源碼:https://github.com/youxiu326/sb_more_submit