本篇内容的内容是实现接口等幂次校验,学习知识追寻者更多springboot系类教程看公众号专辑; 接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的是防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题; 公众号: 知识追寻者
知识追寻者(Inheriting the spirit of open source, Spreading technology knowledge;)
主流的实现方案如下
给表加唯一索引,次方法最简单,当数据重复插入时,直接报SQL异常,对应用影响不大;
alter table 表名 add unique(字段)
示例,两个字段为唯一索引,如果出现完全一样的 order_name, create_time 就直接重复报异常;
alter table `order` add unique(order_name,create_time) 复制代码
分布式锁也可以实现接口等幂次校验,知识追寻者有写过一篇使用redis实现分布式锁思路的一篇文件,小伙伴们可以参考下 《为什么你不会redis分布式锁?因为你没看到这篇文章》
使用乐观锁(基于版本号实现),或者 悲观锁(表锁或者行锁)实现;
入库时先查询是否有该数据,无插入,否则不插入;
token 机制 也就是本篇文章的重点;大致实现思路就是 发起请求的时候先去 redis 获取 token , 将获取的token 放入 请求的hearder , 当请求到达服务端的时候拦截请求,对请求的 hearder 中的token,进行校验,如果校验通过则 放开拦截,删除token,否则 使用自定义异常返回错误信息;
关于 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 void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /** * 普通缓存获取 * @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; } } } 复制代码
使用 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 redisUtils.del(TOKEN_NAME); return true; } } 复制代码
定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求;
/** * @Author lsc * <p> </p> */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { } 复制代码
选择 前置拦截器,每次请求都校验 到达的方法上是否有等幂性注解,如果有则进行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("/**"); } } 复制代码
对控制层进行编写如下,发起请求时通过 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,"校验成功"); } } 复制代码