幂等性概念:保证唯一的意思 如何防止接口不能重复提交===保证接口幂等性
接口幂等产生原因:1.rpc调用时网络延迟(重试发送请求) 2.表单重复提交
解决思路:redis+token,使用Tonken令牌,保证临时且唯一,将token放入redis中,并设置过期时间
如何使用Token 解决幂等性,步骤:
1.在调接口之前生成对应的令牌(Token),存放在Redis
2.调用接口的时候,将该令牌放入请求头中 | 表单隐藏域中
3.接口获取对应的令牌,如果能够获取该令牌(将当前令牌删除掉)就直接执行该访问的业务逻辑
4.接口获取对应的令牌,如果获取不到该令牌,直接返回请勿重复提交
代码部分,使用AOP自定义注解方式对Token进行验证. 防止表单重复提交中,使用AOP注解方式生成Token
1.rpc调用时网络延迟(重试发送请求)
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入redis的依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.28</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.36</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
application.properties
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=mybatis/**/*Mapper.xml
mybatis.type-aliases-package=com.yz.entity
spring.datasource.url=jdbc:mysql://localhost:3306/test01
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.UUID;
/**
* 生成token,放入redis中
* Created by yz on 2018/7/29.
*/
@Component
public class RedisToken {
@Autowired
private BaseRedisService baseRedisService;
private static final long TOKENTIME = 60*60;
public String getToken(){
String token = "token"+UUID.randomUUID();
baseRedisService.setString(token,token,TOKENTIME);
return token;
}
public boolean checkToken(String tokenKey){
String tokenValue = baseRedisService.getString(tokenKey);
if(StringUtils.isEmpty(tokenValue)){
return false;
}
// 保证每个接口对应的token只能访问一次,保证接口幂等性问题
baseRedisService.delKey(tokenKey);
return true;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 集成封装redis
* Created by yz on 2018/7/29.
*/
@Component
public class BaseRedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key,Object data,Long timeout){
if(data instanceof String){
String value = (String) data;
stringRedisTemplate.opsForValue().set(key,value);
}
if(timeout != null){
stringRedisTemplate.expire(key,timeout,TimeUnit.SECONDS);
}
}
public String getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
public void delKey(String key){
stringRedisTemplate.delete(key);
}
}
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.RedisToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 处理rpc调用请求
* Created by yz on 2018/7/29.
*/
@RestController
public class UserController {
@Autowired
private RedisToken redisToken;
@Autowired
private UserService userService;
@RequestMapping(value = "/createRedisToken")
public String createRedisToken(){
return redisToken.getToken();
}
@RequestMapping(value = "/addUser")
public String addOrder(User user, HttpServletRequest request){
// 获取请求头中的token令牌
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
return "参数错误";
}
// 校验token
boolean isToken = redisToken.checkToken(token);
if(!isToken){
return "请勿重复提交!";
}
// 业务逻辑
int result = userService.addUser(user);
return result >0 ? "添加成功" : "添加失败";
}
}
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.yz.mapper")
@SpringBootApplication
public class YzApplication {
public static void main(String[] args) {
SpringApplication.run(YzApplication.class, args);
}
}
测试效果:
获取token
请求接口
再次请求:
将代码改造成AOP注解方式实现
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 解决接口幂等性问题,支持网络延迟和表单提交
* Created by yz on 2018/7/29.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
// 区分请求来源
String type();
}
import com.yz.annotation.CheckToken;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 接口幂等切面
* Created by yz on 2018/7/29.
*/
@Aspect
@Component
public class ExtApiAopIdempotent {
@Autowired
private RedisToken redisToken;
// 切入点,拦截所有请求
@Pointcut("execution(public * com.yz.controller.*.*(..))")
public void rlAop(){}
// 环绕通知拦截所有访问
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 判断方法上是否有加ExtApiAopIdempotent注解
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
CheckToken declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(CheckToken.class);
if(declaredAnnotation != null){
String type = declaredAnnotation.type();
String token = null;
HttpServletRequest request = getRequest();
if(type.equals(ConstantUtils.EXTAPIHEAD)){
// 获取请求头中的token令牌
token = request.getHeader("token");
}else{
// 从表单中获取token
token = request.getParameter("token");
}
if(StringUtils.isEmpty(token)){
return "参数错误";
}
// 校验token
boolean isToken = redisToken.checkToken(token);
if(!isToken){
return "请勿重复提交!";
}
}
// 放行
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
public HttpServletRequest getRequest(){
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
public void response(String msg)throws IOException{
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type","text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.print(msg);
} finally {
writer.close();
}
}
}
controller使用@CheckToken注解:
import com.yz.annotation.CheckToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
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;
/**
* 处理rpc调用请求
* Created by yz on 2018/7/29.
*/
@RestController
public class UserController {
@Autowired
private RedisToken redisToken;
@Autowired
private UserService userService;
@RequestMapping(value = "/createRedisToken")
public String createRedisToken(){
return redisToken.getToken();
}
// 使用CheckToken注解方式保证请求幂等性
@RequestMapping(value = "/addUser")
@CheckToken(type = ConstantUtils.EXTAPIHEAD)
public String addOrder(User user, HttpServletRequest request){
// 业务逻辑
int result = userService.addUser(user);
return result >0 ? "添加成功" : "添加失败";
}
}
执行效果:
2.表单重复提交
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8
index.jsp
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/addUserForPage" method="post">
<input type="hidden" id="token" name="token" value="${token}">
name: <input id="name" name="name" />
<p>
age: <input id="age" name="age" />
<p>
<input type="submit" value="submit" />
</form>
</body>
</html>
import com.yz.annotation.CheckToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* 处理表单提交请求
* Created by yz on 2018/7/29.
*/
@Controller
public class UserPageController {
@Autowired
private RedisToken redisToken;
@Autowired
private UserService userService;
/**
* 页面跳转
* @param req
* @return
*/
@RequestMapping("/indexPage")
public String indexPage(HttpServletRequest req){
req.setAttribute("token",redisToken.getToken());
return "index";
}
// 使用CheckToken注解方式保证请求幂等性
@RequestMapping(value = "/addUserForPage")
@CheckToken(type = ConstantUtils.EXTAPIFROM)
@ResponseBody
public String addOrder(User user, HttpServletRequest request){
// 业务逻辑
int result = userService.addUser(user);
return result >0 ? "添加成功" : "添加失败";
}
}
自定义注解生成Token,将 req.setAttribute("token",redisToken.getToken()); 放在AOP中,减少代码冗余:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解生成Token
* Created by yz on 2018/7/29.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreatToken {
}
// 切入点,拦截所有请求
@Pointcut("execution(public * com.yz.controller.*.*(..))")
public void rlAop(){}
// 前置通知,生成Token
@Before("rlAop()")
public void before(JoinPoint point){
MethodSignature signature = (MethodSignature) point.getSignature();
CreatToken declaredAnnotation = signature.getMethod().getDeclaredAnnotation(CreatToken.class);
if(declaredAnnotation != null){
getRequest().setAttribute("token",redisToken.getToken());
}
}
import com.yz.annotation.CheckToken;
import com.yz.annotation.CreatToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* 处理表单提交请求
* Created by yz on 2018/7/29.
*/
@Controller
public class UserPageController {
@Autowired
private UserService userService;
/**
* 页面跳转,使用自定义注解生成token,传递到跳转页面中
* @param req
* @return
*/
@RequestMapping("/indexPage")
@CreatToken
public String indexPage(HttpServletRequest req){
//req.setAttribute("token",redisToken.getToken());
return "index";
}
// 使用CheckToken注解方式保证请求幂等性
@RequestMapping(value = "/addUserForPage")
@CheckToken(type = ConstantUtils.EXTAPIFROM)
@ResponseBody
public String addOrder(User user, HttpServletRequest request){
// 业务逻辑
int result = userService.addUser(user);
return result >0 ? "添加成功" : "添加失败";
}
}
请求页面的时候,AOP注解会将创建好的token传入到页面中: