当前位置:网站首页>互联网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.Driverimport 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传入到页面中:



边栏推荐
- Jd.com interviewer asked: what is the difference between using on or where in the left join association table and conditions
- 【Go实战基础】如何安装和使用 gin
- Redis installation and deployment (windows/linux)
- Chrome browser tag management plug-in – onetab
- Long summary (code with comments) number structure (C language) -- Chapter 4, string (Part 1)
- 自定義Redis連接池
- 京东高级工程师开发十年,编写出:“亿级流量网站架构核心技术”
- Machine learning practice: is Mermaid a love movie or an action movie? KNN announces the answer
- Methods of classfile
- 西瓜书--第五章.神经网络
猜你喜欢

Matplotlib剑客行——初相识Matplotlib

Say goodbye to 996. What are the necessary plug-ins in idea?

自定義Redis連接池

2022/2/13 summary

盘点典型错误之TypeError: X() got multiple values for argument ‘Y‘

Microservice practice | Eureka registration center and cluster construction

Probability is not yet. Look at statistical learning methods -- Chapter 4, naive Bayesian method

别找了,Chrome浏览器必装插件都在这了

Cloudrev self built cloud disk practice, I said that no one can limit my capacity and speed

深入剖析JVM是如何执行Hello World的
随机推荐
[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)
[go practical basis] how to verify request parameters in gin
双非本科生进大厂,而我还在底层默默地爬树(上)
Taking the upgrade of ByteDance internal data catalog architecture as an example, talk about the performance optimization of business system
Statistical learning methods - Chapter 5, decision tree model and learning (Part 1)
机器学习实战:《美人鱼》属于爱情片还是动作片?KNN揭晓答案
Chrome browser plug-in fatkun installation and introduction
Programmers with ten years of development experience tell you, what core competitiveness do you lack?
[go practical basis] how can gin get the request parameters of get and post
Matplotlib剑客行——没有工具用代码也能画图的造型师
Shengshihaotong and Guoao (Shenzhen) new energy Co., Ltd. build the charging pile industry chain
JVM指令助记符
Chrome浏览器插件-Fatkun安装和介绍
别找了,Chrome浏览器必装插件都在这了
分布式服务架构精讲pdf文档:原理+设计+实战,(收藏再看)
Difference between redis serialization genericjackson2jsonredisserializer and jackson2jsonredisserializer
From concept to method, the statistical learning method -- Chapter 3, k-nearest neighbor method
数构(C语言)——第四章、矩阵的压缩存储(下)
Complete solution of servlet: inheritance relationship, life cycle, container, request forwarding and redirection, etc
队列管理器running状态下无法查看通道