文章目录
实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis
, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除
, 则不能通过校验, 返回请勿重复操作提示
如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
yaml
配置Redis
spring:redis:host: 192.168.111.101port:6379lettuce:pool:max-active:8# 最大连接max-idle:8# 最大空闲连接min-idle:0# 最小空闲连接max-wait:100# 连接等待时间
ResultVo
返回的结果!!!
publicclassResultVo{privateint code;privateString message;privateString data;privateboolean success;publicResultVo(){}publicintgetCode(){return code;}publicvoidsetCode(int code){this.code= code;}publicStringgetMessage(){return message;}publicvoidsetMessage(String message){this.message= message;}publicStringgetData(){return data;}publicvoidsetData(String data){this.data= data;}publicbooleanisSuccess(){return success;}publicvoidsetSuccess(boolean success){this.success= success;}publicstaticResultVook(String data){ResultVo resultVo=newResultVo();
resultVo.setCode(200);
resultVo.setMessage("Success");
resultVo.setSuccess(true);
resultVo.setData(data);return resultVo;}publicstaticResultVofail(String message){ResultVo resultVo=newResultVo();
resultVo.setCode(110);
resultVo.setMessage(message);
resultVo.setSuccess(false);return resultVo;}}
ApiIdempotent
接口
幂等性注解
importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**
* 防重复提交的注解
* 放在Controller类:表示当前类的所有接口都是幂等性
* 放在方法上:表示当前方法是幂等性
*/@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceApiIdempotent{}
ApiIdempotentInterceptor
拦截器,
加了注解的方法都拦截住!!!
importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.web.method.HandlerMethod;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.lang.reflect.Method;/**
* 拦截器
*/@ComponentpublicclassApiIdempotentInterceptorimplementsHandlerInterceptor{@AutowiredprivateTokenService tokenService;@AutowiredprivateTestController testController;/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{if(!(handlerinstanceofHandlerMethod)){returntrue;}HandlerMethod handlerMethod=(HandlerMethod) handler;Method method= handlerMethod.getMethod();//被ApiIdempotment标记的扫描ApiIdempotent methodAnnotation= method.getAnnotation(ApiIdempotent.class);if(methodAnnotation!=null){
tokenService.checkToken(request);}//必须返回true,否则会被拦截一切请求returntrue;}}
WebConfiguration
配置拦截器以及跨域
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.cors.CorsConfiguration;importorg.springframework.web.cors.UrlBasedCorsConfigurationSource;importorg.springframework.web.filter.CorsFilter;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;importjavax.annotation.Resource;@ConfigurationpublicclassWebConfigurationimplementsWebMvcConfigurer{@ResourceprivateApiIdempotentInterceptor apiIdempotentInterceptor;/**
* 跨域
* @return
*/@BeanpublicCorsFiltercorsFilter(){finalUrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource=newUrlBasedCorsConfigurationSource();finalCorsConfiguration corsConfiguration=newCorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);returnnewCorsFilter(urlBasedCorsConfigurationSource);}@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){// 接口幂等性拦截器
registry.addInterceptor(apiIdempotentInterceptor);}}
TokenService
定义
生成token和检查token
importjavax.servlet.http.HttpServletRequest;publicinterfaceTokenService{/**
* 创建token
* @return
*/publicStringcreateToken();/*
*检验token
* */publicvoidcheckToken(HttpServletRequest request);}
TokenServiceImpl
Redis的UUID、token的
创建与检查
importcn.hutool.core.util.StrUtil;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjavax.servlet.http.HttpServletRequest;importjava.time.LocalDateTime;importjava.time.ZoneOffset;importjava.util.concurrent.TimeUnit;@ServicepublicclassTokenServiceImplimplementsTokenService{/**
* 开始时间戳2022-10-1
*/privatefinallong BEGIN_TIMESTAMP=1664553600L;/**
* 序列号的位数
*/privatefinalint COUNT_BITS=32;privatefinalString TOKEN_NAME="IdempotentToken";privatefinalString TOKEN_PREFIX= TOKEN_NAME+":";@AutowiredprivateStringRedisTemplate stringRedisTemplate;/**
* 创建token
*
* @return
*/@OverridepublicStringcreateToken(){//使用Redis生成tokenString token=nextId();StringBuilder sb=newStringBuilder();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();}returnnull;}//删除Token,true表示第一次提交,false表示重复提交publicBooleandeleteToken(String token){return stringRedisTemplate.delete(TOKEN_PREFIX+token);}publicvoidcheckToken(HttpServletRequest request){//获取请求头携带的tokenString 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));}publicStringnextId(){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
进行测试的接口!!!
importcn.hutool.json.JSONUtil;importorg.springframework.web.bind.annotation.*;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;@RestControllerpublicclassTestController{@ResourceprivateTokenService tokenService;@PostMapping("/get/token")publicStringgetToken(){String token= tokenService.createToken();if(!token.isEmpty()){returnJSONUtil.toJsonStr(ResultVo.ok(token));}returnStrUtil.EMPTY;}@PostMapping("/add")@ApiIdempotentpublicStringadd(HttpServletRequest request){//userService.insertUser(user); //有字段唯一性,故注释ResultVo msg=(ResultVo) request.getAttribute("msg");String str=JSONUtil.toJsonStr(msg);System.out.println(str);return str;}}
测试
获取令牌
Redis中可以看到令牌!
不带请求头或请求头错误
带请求头
完成!!!