• springboot实现接口等幂性校验


    一 前言

    本篇内容的内容是实现接口等幂次校验,学习知识追寻者更多springboot系类教程看公众号专辑; 接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的是防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题;

    公众号:知识追寻者

    知识追寻者(Inheriting the spirit of open source, Spreading technology knowledge;)

    二 实现方案

    主流的实现方案如下

    2.1 唯一索引

    给表加唯一索引,次方法最简单,当数据重复插入时,直接报SQL异常,对应用影响不大;

    alter table 表名 add unique(字段)

    示例,两个字段为唯一索引,如果出现完全一样的 order_name, create_time 就直接重复报异常;

    alter table `order`  add unique(order_name,create_time)
    

    2.2 锁

    分布式锁也可以实现接口等幂次校验,知识追寻者有写过一篇使用redis实现分布式锁思路的一篇文件,小伙伴们可以参考下《为什么你不会redis分布式锁?因为你没看到这篇文章》

    使用乐观锁(基于版本号实现),或者 悲观锁(表锁或者行锁)实现;

    2.3 先查询后判断

    入库时先查询是否有该数据,无插入,否则不插入;

    2.4 token 机制

    token 机制 也就是本篇文章的重点;大致实现思路就是 发起请求的时候先去 redis 获取 token , 将获取的token 放入 请求的hearder , 当请求到达服务端的时候拦截请求,对请求的 hearder 中的token,进行校验,如果校验通过则 放开拦截,删除token,否则 使用自定义异常返回错误信息;

    三 使用redis 实现 接口等幂性校验

    3.1 redis 工具类

    关于 RedisTemplate 的配置可以参考知识追寻者发布的文章 《springboot集成redis(基础篇)》

    /**
     * @Author lsc
     * <p> </p>
     */
    
    @Component
    public class RedisUtils {
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        /**
         * 判断key是否存在
         * @param key 键
         * @return boolean
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
    
        /**
         * 删除key
         * @param key 键
         */
        public Boolean del(String key) {
            if (key != null && key.length() > 0) {
                return redisTemplate.delete(key);
            }else  {
                return false;
            }
        }
    
    
        /**
         * 普通缓存获取
         * @param key 键
         * @return 值
         */
    
        public Object get(String key) {
    
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
    
        /**
         * 普通缓存放入并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
    
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
    }
    

    3.2 token 工具类

    使用 uuid 生成 随机字符串,通过md5加密防止token被解密,保证token的唯一性与安全性;,设置过期时间为 30 秒,即在30秒内只能提交成功一次请求,根据不同的业务需求,读者们自行处理;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Component
    public class TokenUtis {
    
        @Autowired
        RedisUtils redisUtils;
    
        // token 过期时间为30秒
        private final static Long TOKEN_EXPIRE = 30L;
    
        private final static String TOKEN_NAME = "token";
        /* *
         * @Author lsc
         * <p> 生成token 放入缓存</p>
         * @Param []
         */
        public String generateToken() {
            String uuid = UUID.randomUUID().toString();
            String token = DigestUtils.md5DigestAsHex(uuid.getBytes());
            redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE);
            return token;
        }
        /* *
         * @Author lsc
         * <p> token 校验 </p>
         * @Param [request]
         */
        public boolean verifyToken(HttpServletRequest request) {
            String token = request.getHeader(TOKEN_NAME);
            // header中不存在token
            if(StringUtils.isEmpty(token)) {
               // 抛出自定义异常
                System.out.println("token不存在");
                throw new GlobleException(CodeMsg.BAD_REQUEST);
            }
            // 缓存中不出在 
            if(!redisUtils.hasKey(TOKEN_NAME)) {
                // 抛出自定义异常
                System.out.println("token已经过期");
                throw new GlobleException(CodeMsg.BAD_REQUEST);
            }
            String cachToekn = (String)redisUtils.get(TOKEN_NAME);
            if (!token.equals(cachToekn)){
                // 抛出自定义异常
                System.out.println("token校验失败");
                throw new GlobleException(CodeMsg.BAD_REQUEST);
            }
            // 移除token
            Boolean del = redisUtils.del(TOKEN_NAME);
            if (!del){
                // 抛出自定义异常
                System.out.println("token删除失败");
                throw new GlobleException(CodeMsg.BAD_REQUEST);
            }
            return true;
        }
    }
    

    3.3 注解

    定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
    }
    

    3.4 拦截器配置

    选择 前置拦截器,每次请求都校验 到达的方法上是否有等幂性注解,如果有则进行token 校验;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Component
    public class IdempotentInterceptor implements HandlerInterceptor {
    
        @Autowired
        private TokenUtis tokenUtis;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
            // 对有Idempotent注解的方法进行拦截校验
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
            if (methodAnnotation != null) {
                // token 校验
                tokenUtis.verifyToken(request);
            }
            return true;
        }
    
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }
    

    对拦截器进行url模式匹配,并注入spring容器

    /**
     * @Author lsc
     * <p> </p>
     */
    @Configuration
    public class WebConfiguration implements WebMvcConfigurer {
    
        @Autowired
        IdempotentInterceptor idempotentInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 拦截所有请求
            registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**");
        }
    
    }
    

    3.5 控制层

    对控制层进行编写如下,发起请求时通过 getToken 方法获取token,将获取的token 放入 hearder 后 再请求 testIdempotent 方法

    /**
     * @Author lsc
     * <p> </p>
     */
    @RestController
    public class ZszxzController {
    
        @Autowired
        TokenUtis tokenUtis;
    
        @GetMapping("zszxz/token")
        public ResultPage getToken(){
            String token = tokenUtis.generateToken();
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("token",token);
            return ResultPage.sucess(CodeMsg.SUCESS,jsonObject);
        }
    
        @Idempotent
        @GetMapping("zszxz/test")
        public ResultPage testIdempotent(){
            return ResultPage.sucess(CodeMsg.SUCESS,"校验成功");
        }
    }
    

    3.6 测试

    请求获取token

    NHm27D.png

    已经被消耗的token,请求时报错

    NHnpj0.md.png

    重新获取token 请求成功

    NHnKu6.md.png

    对于高并发请求可以使用jmeter进行测试,本篇文章也可以使用aop拦截实现;

  • 相关阅读:
    &lt;Android&gt;greenrobot-EventBus,guava-Event Bus的异步实现
    MySQL Study之--MySQL用户及权限管理
    关于Apacheserver的訪问控制
    Jquery改动页面标题title其他JS失效
    P3742 umi的函数
    P3717 [AHOI2017初中组]cover
    P2261 [CQOI2007]余数求和
    P1423 小玉在游泳
    P1888 三角函数
    3295 落单的数 九章算法面试题
  • 原文地址:https://www.cnblogs.com/zszxz/p/13222122.html
Copyright © 2020-2023  润新知