文章目录
实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis
, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除
, 则不能通过校验, 返回请勿重复操作提示
如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
yaml
配置Redis
spring:
redis:
host: 192.168.111.101
port: 6379
lettuce:
pool:
max-active: 8 # 最大连接
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 100 # 连接等待时间
ResultVo
返回的结果!!!
public class ResultVo {
private int code;
private String message;
private String data;
private boolean success;
public ResultVo() {
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public static ResultVo ok(String data){
ResultVo resultVo = new ResultVo();
resultVo.setCode(200);
resultVo.setMessage("Success");
resultVo.setSuccess(true);
resultVo.setData(data);
return resultVo;
}
public static ResultVo fail(String message){
ResultVo resultVo = new ResultVo();
resultVo.setCode(110);
resultVo.setMessage(message);
resultVo.setSuccess(false);
return resultVo;
}
}
ApiIdempotent
接口
幂等性注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 防重复提交的注解
* 放在Controller类:表示当前类的所有接口都是幂等性
* 放在方法上:表示当前方法是幂等性
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
ApiIdempotentInterceptor
拦截器,
加了注解的方法都拦截住!!!
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;
import java.lang.reflect.Method;
/**
* 拦截器
*/
@Component
public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Autowired
private TestController testController;
/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
tokenService.checkToken(request);
}
//必须返回true,否则会被拦截一切请求
return true;
}
}
WebConfiguration
配置拦截器以及跨域
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Resource
private ApiIdempotentInterceptor apiIdempotentInterceptor;
/**
* 跨域
* @return
*/
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 接口幂等性拦截器
registry.addInterceptor(apiIdempotentInterceptor);
}
}
TokenService
定义
生成token和检查token
import javax.servlet.http.HttpServletRequest;
public interface TokenService {
/**
* 创建token
* @return
*/
public String createToken();
/*
*检验token
* */
public void checkToken(HttpServletRequest request);
}
TokenServiceImpl
Redis的UUID、token的
创建与检查
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
@Service
public class TokenServiceImpl implements TokenService {
/**
* 开始时间戳2022-10-1
*/
private final long BEGIN_TIMESTAMP = 1664553600L;
/**
* 序列号的位数
*/
private final int COUNT_BITS = 32;
private final String TOKEN_NAME = "IdempotentToken";
private final String TOKEN_PREFIX = TOKEN_NAME + ":";
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
//使用Redis生成token
String token = nextId();
StringBuilder sb = new StringBuilder();
try {
//token的生成规则
sb.append(TOKEN_PREFIX).append(token);
//token三分钟过期
stringRedisTemplate.opsForValue().set(sb.toString(), "1", 180L, TimeUnit.SECONDS);
boolean isNotEmpty = !sb.toString().isEmpty();
if (isNotEmpty) {
return sb.toString();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
//删除Token,true表示第一次提交,false表示重复提交
public Boolean deleteToken(String token) {
return stringRedisTemplate.delete(TOKEN_PREFIX+token);
}
public void checkToken(HttpServletRequest request){
//获取请求头携带的token
String token = request.getHeader(TOKEN_NAME);
//System.out.println(TOKEN_PREFIX+token);
//如果没有携带token,抛异常
if (StrUtil.isEmpty(token)) {
request.setAttribute("msg",ResultVo.fail("重复提交"));
return;
}
//幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回提示
Boolean flag = deleteToken(token);
if (Boolean.FALSE.equals(flag)) {
//重复提交
request.setAttribute("msg",ResultVo.fail("重复提交"));
return;
}
request.setAttribute("msg",ResultVo.ok(token));
}
public String nextId() {
String key = "UUID:count";
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.自增长
long count = stringRedisTemplate.opsForValue().increment(key);
// 3.可以设置过期时间
stringRedisTemplate.expire(key, 60, TimeUnit.DAYS);
// 4.拼接并返回
return (timestamp << COUNT_BITS | count) + "";
}
}
TestController
进行测试的接口!!!
import cn.hutool.json.JSONUtil;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@RestController
public class TestController {
@Resource
private TokenService tokenService;
@PostMapping("/get/token")
public String getToken() {
String token = tokenService.createToken();
if (!token.isEmpty()) {
return JSONUtil.toJsonStr(ResultVo.ok(token));
}
return StrUtil.EMPTY;
}
@PostMapping("/add")
@ApiIdempotent
public String add(HttpServletRequest request) {
//userService.insertUser(user); //有字段唯一性,故注释
ResultVo msg = (ResultVo) request.getAttribute("msg");
String str = JSONUtil.toJsonStr(msg);
System.out.println(str);
return str;
}
}
测试
获取令牌
Redis中可以看到令牌!
不带请求头或请求头错误
带请求头
完成!!!