springboot + redis + 注解 + 拦截器用Token 实现接口幂等性

2022-10-31 12:37:20

实现思路

为需要保证幂等性的每一次请求创建一个唯一标识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中可以看到令牌!

在这里插入图片描述

不带请求头或请求头错误

在这里插入图片描述

带请求头

在这里插入图片描述

完成!!!

  • 作者:Forever Nore
  • 原文链接:https://blog.csdn.net/qq_30659573/article/details/127231089
    更新时间:2022-10-31 12:37:20