springboot token_Springboot接口幂等性基于token实现方案

什么是接口幂等

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中,即f(f(x)) = f(x).简单的来说就是一个操作多次执行产生的结果与一次执行产生的结果一致。有些系统操作天生就具有幂等性例如数据库的select语句,但更多时候是需要程序员来做保证的,尤其是在分布式系统环境中,接口能不能做到保证幂等性对系统的影响可能是非常大的,例如很常见的支付下单等场景,由于分布式环境中网络的复杂性,用户误操作,网络抖动,消息重复,服务超时导致业务自动重试等等各种情况都可能会使线上数据产生了不一致,造成生产事故。

实现方案

1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;3、唯一索引:利用数据库新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);4、token机制:防止页面重复提交。采用token加redis或token加jvm内存。处理流程:1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间;2. 提交后后台校验token,同时删除token,生成新的token返回。token特点:要申请,一次有效性,可以限流。注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用;5、分布式锁:如果是分布式系统的话,构建全局唯一索引会比较困难,比如唯一性的字段就没有办法确定。这时候可以引入分布式锁,通过第三方的系统(Redis或Zookeeper),在业务系统插入数据或者更新数据前,需要先获取分布式锁,然后才能做操作,操作完成之后就释放锁。这样其实是把单机系统里面多线程并发锁的思路引入了多个系统的场景,也就是分布式系统中的解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。6、select + insert:在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图)。简单理解,就是业务单据上面有个状态的字段,状态在不同的情况下会发生变更,一般情况下存在有限状态机。这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助。

基于token+Redis的实现方案

环境:springboot2.2.11.RELEASE + Redis

  • pom.xml 依赖

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
  • 自定义注解类,有该注解的需要验证token是否有效

@Documented
@Inherited
@Retention(RUNTIME)
@Target({ METHOD, TYPE})
public @interface ApiIdempotent {

}
  • 拦截器定义,拦截请求方法进行token有效性验证

public class MethodIdempotentCheck implements HandlerInterceptor {

private Logger logger = LoggerFactory.getLogger(MethodIdempotentCheck.class) ;

@Resource
private StringRedisTemplate stringRedisTemplate ;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler ;
Method method = handlerMethod.getMethod() ;
Class> clazz = method.getClass() ;
if (clazz.isAnnotationPresent(ApiIdempotent.class)) {
if (!checkToken(request)) {
failure(response) ;
return false ;
}
} else {
if (method.isAnnotationPresent(ApiIdempotent.class)) {
if (!checkToken(request)) {
failure(response) ;
return false ;
}
}
}
}
return true ;
}

private void failure(HttpServletResponse response) throws Exception {
response.setContentType("application/json;charset=utf-8") ;
response.getWriter().write("{\"code\": -1, \"message\": \"重复提交\"}") ;
}

private boolean checkToken(HttpServletRequest request) {
logger.info("验证token") ;
String token = request.getParameter("access-token") ;
if (token == null || token.length() == 0) {
token = request.getHeader("access-token") ;
}
logger.info("获取token:{}", token) ;
if (token == null || token.length() == 0) {
return false ;
}
boolean exists = stringRedisTemplate.hasKey(token) ;
if (!exists) {
return false ;
}
return stringRedisTemplate.delete(token) ;
}

}
token会从header中获取请求参数中获取。
  • WebConfig 配置拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor()) ;
}

@Bean
public HandlerInterceptor tokenInterceptor() {
return new MethodIdempotentCheck() ;
}

}
  • Controller 测试

@RestController
@RequestMapping("/demo")
public class DemoController {

@Resource
private StringRedisTemplate stringRedisTemplate ;

/**
* 生成Token;这里token的有效期设置了10分钟。
* @return
*/
@GetMapping("/create")
public Object create() {
String access_token = UUID.randomUUID().toString() ;
stringRedisTemplate.opsForValue().set(access_token, access_token, 10, TimeUnit.MINUTES) ;
return access_token ;
}

@PostMapping("/save")
@ApiIdempotent
public Object business(@RequestBody Users user) {
Map result = new HashMap<>() ;// todo save Users
result.put("code", 0) ;
result.put("message", "创建成功") ;return result ;
}
}

business 方法加入了@ApiIdempotent注解,表示该方法需要进行token验证是否有效的请求

整个请求流程要先获取token,然后将得到的token放入header中或者请求参数中。

测试:

获取token:

1dbc1e136f9731b43c760ac796eb2df4.png

请求业务方法将获取的token 添加到header中

769c2e35f782f7bd16d2b09e73d64c1e.png

4269b94af85d5b5843b5d95e38f27740.png

再次请求:

6bbd18292378b1a5a381a547963a1b47.png

每次请求token的验证是通过删除token进行的,所以当第二次再请求时,redis中已经没有了token所以这里就提示:重复提交了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值