当前位置:网站首页>互联网API接口幂等设计
互联网API接口幂等设计
2022-07-02 06:34:00 【niceyz】
幂等性概念:保证唯一的意思 如何防止接口不能重复提交===保证接口幂等性
接口幂等产生原因: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传入到页面中:
边栏推荐
- Win10 uses docker to pull the redis image and reports an error read only file system: unknown
- Enterprise level SaaS CRM implementation
- ClassFile - Attributes - Code
- [staff] common symbols of staff (Hualian clef | treble clef | bass clef | rest | bar line)
- Attributes of classfile
- Mysql 多列IN操作
- [go practical basis] how can gin get the request parameters of get and post
- Mathematics in machine learning -- point estimation (I): basic knowledge
- MySQL multi column in operation
- Matplotlib剑客行——容纳百川的艺术家教程
猜你喜欢
Solutions to Chinese garbled code in CMD window
十年开发经验的程序员告诉你,你还缺少哪些核心竞争力?
[go practical basis] how can gin get the request parameters of get and post
Hystrix implements request consolidation
「Redis源码系列」关于源码阅读的学习与思考
Programmers with ten years of development experience tell you, what core competitiveness do you lack?
oracle修改数据库字符集
"Redis source code series" learning and thinking about source code reading
定时线程池实现请求合并
微服务实战|微服务网关Zuul入门与实战
随机推荐
Chrome视频下载插件–Video Downloader for Chrome
Don't look for it. All the necessary plug-ins for Chrome browser are here
Oracle modify database character set
AMQ6126问题解决思路
【Go实战基础】如何安装和使用 gin
概念到方法,绝了《统计学习方法》——第三章、k近邻法
Knowledge points are very detailed (code is annotated) number structure (C language) -- Chapter 3, stack and queue
Avoid breaking changes caused by modifying constructor input parameters
定时线程池实现请求合并
Data type case of machine learning -- using data to distinguish men and women based on Naive Bayesian method
Statistical learning methods - Chapter 5, decision tree model and learning (Part 1)
How to choose between efficiency and correctness of these three implementation methods of distributed locks?
Insight into cloud native | microservices and microservice architecture
[staff] time mark and note duration (staff time mark | full note rest | half note rest | quarter note rest | eighth note rest | sixteenth note rest | thirty second note rest)
MySql报错:unblock with mysqladmin flush-hosts
Number structure (C language -- code with comments) -- Chapter 2, linear table (updated version)
MySQL error: unblock with mysqladmin flush hosts
分布式锁的这三种实现方式,如何在效率和正确性之间选择?
Complete solution of servlet: inheritance relationship, life cycle, container, request forwarding and redirection, etc
Watermelon book -- Chapter 6 Support vector machine (SVM)