java后端使用token处理表单重复提交
- 保证接口幂等性,表单重复提交
前台解决方案:
提交后按钮禁用、置灰、页面出现遮罩
后台解决方案: 使用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.connection.RedisConnection; import org.springframework.data.redis.connection.ReturnType; 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; private static final String LUASCRIPT = "if redis.call('exists', KEYS[1]) == 0 then " + "return 0; " + "else " + "redis.call('del', KEYS[1]); " + "return 1; " + "end;"; /** * 获取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 */ @Deprecated public boolean findTokenOld(String tokenKey){ String token = (String) redisTemplate.opsForValue().get(tokenKey); if (StringUtils.isEmpty(token)) { return false; } // token 获取成功后 删除对应tokenMapstoken redisTemplate.delete(tokenKey); return true; } /** * 判断Token是否存在 并且删除Token * @param tokenKey * @return */ public boolean findToken(String tokenKey){ Boolean existKey = (Boolean) redisTemplate.execute( (RedisConnection connection) -> connection.eval( LUASCRIPT.getBytes(), //lua脚本 ReturnType.BOOLEAN, //设置返回 布尔值 1, //设置key数量 tokenKey.getBytes() ) ); return existKey; } }
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