redis使用token令牌处理接口幂等性
1.方案描述
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。
2.注意事项
需要生成全局唯一 Token 串;
需要使用第三方组件 Redis 进行数据效验;
3.主要流程
① 服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串。
② 客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
③ 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
④ 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
⑤ 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
⑥ 服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
⑦ 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
4.具体实现
一:在配置文件中配置redis信息,这里使用的是yaml来配置
二:创建一个用来处理幂等性的注解,将来哪个接口需要处理幂等性,哪个接口就添加这个注解
package com.jdzh.enterprise.framework.service.redis;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
三:service层代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author Heb
* @version 1.0
* @description: TODO
* @date 2023/3/6 14:28
*/
@Service
public class RedisService {
@Autowired
StringRedisTemplate redisTemplate;
public boolean set(String key, String value, int time, TimeUnit timeoutUtils){
try {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(key, value);
redisTemplate.expire(key, time, timeoutUtils);
} catch (Exception exception) {
exception.printStackTrace();
return false;
}
return true;
}
public Boolean exists(String key){
return redisTemplate.hasKey(key);
}
public boolean delete(String key){
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author Heb
* @version 1.0
* @description: TODO
* @date 2023/3/6 14:37
*/
@Service
public class TokenService {
@Autowired
RedisService redisService;
/**
* 生成令牌并存入redis并返回
* @return
*/
public String generateToken(){
String token = UUID.randomUUID().toString();
redisService.set(token,token,30, TimeUnit.MINUTES);
return token;
}
/**
* 检查前端请求传来的token
* @param req
* @return
*/
public boolean checkToken(HttpServletRequest req){
String token = req.getHeader("token");
if(token==null||"".equals("token")){
token=req.getParameter("token");
if(token==null||"".equals(token)) {
throw new RuntimeException("请求令牌不合法");
}
}
Boolean exists = redisService.exists(token);
if(exists){
//说明redis有令牌
redisService.delete(token);
return true;
}
//redis没有令牌,说明是非法请求或者是请求已被处理
return false;
}
}
四:编写一个过滤器,通过这个过滤器, 获取被拦截方法的注解,判断该方法是否需要去处理接口幂等性
import com.jdzh.enterprise.framework.service.redis.Idempotent;
import com.jdzh.enterprise.framework.service.redis.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Heb
* @version 1.0
* @description: TODO
* @date 2023/3/6 14:51
*/
@Component
public class IdemponentIntercepter implements HandlerInterceptor {
@Autowired
TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取被拦截方法的注解
Idempotent methodAnnotation = handlerMethod.getMethodAnnotation(Idempotent.class);
if(methodAnnotation!=null){
//说明方法有这个注解
boolean b = tokenService.checkToken(request);
if(!b){
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("请求重复");
}
return b;
}else{
//没有这个注解
return true;
}
}else{
return true;
}
}
}
五:编写配置类,拦截所有请求
import com.jdzh.enterprise.framework.intercepter.IdemponentIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Heb
* @version 1.0
* @description: TODO
* @date 2023/3/6 15:00
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
IdemponentIntercepter intercepter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(intercepter).addPathPatterns("/**");
}
}
5.测试
一:在需要处理接口幂等性的接口上添加注解
二:前端定义一个token变量,发起请求从后端获取token,并将token赋值
<template>
<view>
<button @click="gettoken()">获取token</button>
</view>
</template>
data() {
return {
title: 'Hello',
token:'',
},
methods: {
gettoken(){
uni.request({
method:'GET',
url: 'http://192.168.1.13:8085/wxPay/token', //仅为示例,并非真实接口地址。
data: {
text: 'uni.request'
},
header: {
'custom-header': 'hello' //自定义请求头信息
},
success: (res) => {
this.token=res.data;
}
});
}}
三;将这个token作为请求头,添加在我们需要处理幂等性的请求上
四:这样就处理完成了