1、问题描述
最近在写项目的时候,遇到一个问题:我后端接口Controller中已经检查了重复问题。但是因为网络波动,延迟,用户重复点击,恶意请求等原因。导致数据库中重复插入了两条一模一样的数据。为此经过团队成员商量,决定实现了一个接口幂等性校验。
2、基本逻辑
-
前端用户第一次点击“新增”接口,在拦截器中,
-
在拦截器中:拦截请求,使用【redis缓存前缀+ url+token】作key值,请求参数和Long类型的时间值做value,缓存到redis中,
-
用户第二次点击“新增”接口,在拦截器中根据【redis缓存前缀+ url+token】检查redis是否有数据,如果有就和第二次发送的数据做对比,对比参数是否相同,对比两次的时间是否超时(可以使用注解,也可以全局设置时间)
-
如果判断参数相同,时间也不超时,就提示用户不能重复点击。
3、代码实现
我在这里使用了注解+Springboot拦截器+redis的实现方式;所以我们先定义一个注解。
-
({ElementType.METHOD})(RetentionPolicy.RUNTIME)public AutoIdempotent {/*** 间隔时间(ms),小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default "不允许重复提交,请稍候再试!";}
我在写一个interface,只做检查接口是不是在redis中是否有值
public interface TokenCheckService {/*** 检验token** @param request http请求* @return*/boolean isRepeatSubmit(HttpServletRequest request, AutoIdempotent autoIdempotent) throws Exception;}
实现类:
public class TokenCheckServiceImpl implements TokenCheckService {private final String REPEAT_PARAMS = "repeatParams";private final String REPEAT_TIME = "repeatTime";private RedisUtil redisUtil;public boolean isRepeatSubmit(HttpServletRequest request, AutoIdempotent autoIdempotent) throws Exception {Map<String, String> requestParams = CommonUtil.getRequestParams(request);String accessToken = "";if (!CollectionUtils.isEmpty(requestParams)) {accessToken = requestParams.get(CommonConstant.TOKEN_NAME);}Map<String, Object> nowDataMap = new HashMap<String, Object>();nowDataMap.put(REPEAT_PARAMS, requestParams);nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());// 请求地址(作为存放cache的key值)String url = request.getRequestURI();// 唯一值(没有消息头则使用请求地址)String submitKey = "-" + StringUtils.trimToEmpty(accessToken);// 唯一标识(指定key + url + 消息头)String cacheRepeatKey = CommonConstant.TOKEN_PREFIX + url + submitKey;Object sessionObj = redisUtil.get(cacheRepeatKey);if (sessionObj != null) {Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;if (sessionMap.containsKey(url)) {Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, autoIdempotent.interval())) {return true;}}}Map<String, Object> cacheMap = new HashMap<String, Object>();cacheMap.put(url, nowDataMap);// MILLISECONDS = 毫秒redisUtil.setEx(cacheRepeatKey, cacheMap, autoIdempotent.interval(), TimeUnit.MILLISECONDS);return false;}/*** 判断参数是否相同*/private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {String nowParams = nowMap.get(REPEAT_PARAMS).toString();String preParams = preMap.get(REPEAT_PARAMS).toString();return nowParams.equals(preParams);}/*** 判断两次间隔时间*/private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {long time1 = (Long) nowMap.get(REPEAT_TIME);long time2 = (Long) preMap.get(REPEAT_TIME);if ((time1 - time2) < interval) {return true;}return false;}}
工具类:
/*** 从请求头获取参数* @param request http请求头* @return*/public static Map<String, String> getRequestParams(HttpServletRequest request) {Map<String, String[]> parameterMap = request.getParameterMap();Map<String, String> params = new HashMap<>();for (String key : parameterMap.keySet()) {String[] values = parameterMap.get(key);if (values.length > 0) {params.put(key, values[0]);}}return params;}
实现HandlerInterceptor 拦截器
4jpublic class RepeatedSubmissionInterceptor implements HandlerInterceptor {private TokenCheckService tokenCheckService;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();//扫描被 AutoIdempotent 标记的方法AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);if (methodAnnotation != null) {// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示if (tokenCheckService.isRepeatSubmit(request, methodAnnotation)) {Res ajaxResult = Res.error(methodAnnotation.message());CommonUtil.renderString(response, JSON.toJSONString(ajaxResult));return false;}}//必须返回true,否则会被拦截一切请求return true;}}
注册拦截器,WebMvcConfigrationSupport,WebMvcConfigurer.,这两个里面只能存在一个,因为项目中已经使用了WebMvcConfigrationSupport,所以在使用WebMvcConfigurer注册就不起作用。(注意)
2public class ResourceConfig extends WebMvcConfigurationSupport {private RepeatedSubmissionInterceptor accessLimintInterceptor;protected void addInterceptors(InterceptorRegistry registry) {super.addInterceptors(registry);registry.addInterceptor(accessLimintInterceptor).addPathPatterns("/**");}}
测试Controller
public class SysUserController{private IUserService userService;// 防止用户重复点击注解,有此注解public Res<?> addUser( String username) {this.userService.addUser(username);return Res.success();}}
前端在重复点击的时候。第二次就会提示:


本篇文章来源于微信公众号: 张俊发
微信扫描下方的二维码阅读本文

Comments NOTHING