在开发过程中经常需要做防止重复提交处理,例如:下订单,保存信息等等
前端处理思路:
点击按钮后,立即将按钮置灰且不可使用,然后调用处理逻辑接口,当接口有响应后重新使按钮重新亮起可用
后端处理思路:
思路一、建立数据库唯一索引,通过数据库唯一索引,保证数据唯一
思路二、通过token方式,调用业务接口前先调用接口获取token,调用业务接口时传入token,先进行token校验和处理,当token正确时删除该token(第二次传入相同token就会校验不通过),然后处理正常的业务逻辑
https://github.com/qidasheng2012/springboot2.x_redis
本文token生成后存入Redis,所以在Redis构建的项目基础上进行处理的,Redis项目构建文章:
https://blog.csdn.net/qidasheng2012/article/details/96475737
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.0.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>springboot2.x_redisartifactId>
<version>1.0.0version>
<name>springboot2.x_redisname>
<description>SpringBoot2.x demo project for Redisdescription>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.11.0version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.8.1version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
注意需要Redis相关依赖和AOP的依赖,分布式锁的依赖可以根据项目需求进行添加或删除
用于生成和删除token处理
package com.example.springboot_redis.token;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 生成token和删除token
*/
@Slf4j
@Component
public class ActionToken {
// 默认缓存时间
private final Long TOKEN_EXPIRE_TIME = 60 * 60 * 24L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/*
* 生成token
*/
public String createToken(String sessionId) {
if (StringUtils.isBlank(sessionId)) {
return null;
}
// 使用UUID当token
String token = UUID.randomUUID().toString();
// 存入缓存并设置有效期 TimeUnit.SECONDS 单位:秒
stringRedisTemplate.opsForValue().set(token, sessionId, TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
if (StringUtils.isBlank(stringRedisTemplate.opsForValue().get(token))) {
throw new RuntimeException("生成token缓存redis失败");
}
return token;
}
/*
* 校验token
*/
public String tokenVerify(String token) {
if (StringUtils.isBlank(token)) {
log.info("token 为空");
return "请勿重复提交";
}
String sessionId = stringRedisTemplate.opsForValue().get(token);
if (StringUtils.isBlank(sessionId)) {
log.info("Redis 中 key 为 token 的不存在");
return "请勿重复提交";
}
// token 存在,移除Redis中的token,进入业务逻辑
stringRedisTemplate.delete(token);
log.info("redis 删除key为token:[{}]成功,进入业务逻辑", token);
return "";
}
}
防重复提交注解
package com.example.springboot_redis.token;
import java.lang.annotation.*;
/*
* 防重复提交注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenVerify {
String value() default "";
}
处理防重复提交的切面
package com.example.springboot_redis.token;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 处理防重复提交token切面
*/
@Slf4j
@Aspect
@Component
public class TokenAspect {
@Autowired
private ActionToken actionToken;
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
// 切入点签名
@Pointcut("@annotation(com.example.springboot_redis.token.TokenVerify)")
private void tokenPoint() {
}
// 环绕通知
@Around(value = "tokenPoint()")
public Object tokenVerify(ProceedingJoinPoint joinPoint) throws Throwable {
String msg = actionToken.tokenVerify(request.getParameter("token"));
if (StringUtils.isBlank(msg)) {
// 删除token成功,进入业务逻辑
try {
return joinPoint.proceed();
} catch (Exception e) {
log.error("删除token成功,业务处理异常:", e);
}
} else {
PrintWriter printWriter = null;
try {
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
printWriter = response.getWriter();
printWriter.write(msg);
printWriter.flush();
} catch (Exception e) {
log.error("处理token,返回错误信息时异常", e);
} finally {
IOUtils.closeQuietly(printWriter);
}
}
return null;
}
}
获取token接口和使用@TokenVerify防止重复提交
package com.example.springboot_redis.contoller;
import com.example.springboot_redis.token.ActionToken;
import com.example.springboot_redis.token.TokenVerify;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private ActionToken actionToken;
// 生成token
@RequestMapping("/createToken")
public String createToke(HttpServletRequest request) {
String sessionId = request.getSession().getId();
return actionToken.createToken(sessionId);
}
// 测试@TokenVerify
@TokenVerify
@RequestMapping("/testToken")
public void testToken() {
log.info("正常业务逻辑");
}
}
1、 获取token
http://127.0.0.1/token/createToken
Redis
2、调用测试@TokenVerify方法
http://127.0.0.1/token/testToken?token=9922613d-603d-4fee-bca3-98efb6ba9ed1
Spring Boot+Redis+拦截器+自定义Annotation实现接口自动幂等