1、问题描述

最近在写项目的时候,遇到一个问题:我后端接口Controller中已经检查了重复问题。但是因为网络波动,延迟,用户重复点击,恶意请求等原因。导致数据库中重复插入了两条一模一样的数据。为此经过团队成员商量,决定实现了一个接口幂等性校验。

2、基本逻辑

  •  前端用户第一次点击“新增”接口,在拦截器中,

  • 在拦截器中:拦截请求,使用【redis缓存前缀+ url+token】作key值,请求参数和Long类型的时间值做value,缓存到redis中,

  • 用户第二次点击“新增”接口,在拦截器中根据【redis缓存前缀+ url+token】检查redis是否有数据,如果有就和第二次发送的数据做对比,对比参数是否相同,对比两次的时间是否超时(可以使用注解,也可以全局设置时间)

  • 如果判断参数相同,时间也不超时,就提示用户不能重复点击。


    3、代码实现

    我在这里使用了注解+Springboot拦截器+redis的实现方式;所以我们先定义一个注解。


    @Inherited@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface 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;
}

实现类:

@Slf4j@Servicepublic class TokenCheckServiceImpl implements TokenCheckService {    private final String REPEAT_PARAMS = "repeatParams";    private final String REPEAT_TIME = "repeatTime";    @Autowired    private RedisUtil redisUtil;
@Override 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 拦截器

@Slf4j@Componentpublic class RepeatedSubmissionInterceptor implements HandlerInterceptor {    @Autowired    private TokenCheckService tokenCheckService;
@Override 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注册就不起作用。(注意

@Configuration@EnableSwagger2public class ResourceConfig extends WebMvcConfigurationSupport {    @Resource    private RepeatedSubmissionInterceptor accessLimintInterceptor;        @Override    protected void addInterceptors(InterceptorRegistry registry) {        super.addInterceptors(registry);        registry.addInterceptor(accessLimintInterceptor).addPathPatterns("/**");    }}

测试Controller

@Api(tags = "用户管理")@RestController@RequestMapping("/sys")public class SysUserController{    @Autowired    private IUserService userService;
@SysLog(title = "新建用户", businessType = BusinessType.INSERT) @ApiOperation(value = "新建用户") @PostMapping("/addUser") @AutoIdempotent // 防止用户重复点击注解,有此注解 public Res<?> addUser(@RequestParam(name = "username") String username) { this.userService.addUser(username); return Res.success(); }
}

前端在重复点击的时候。第二次就会提示:


本篇文章来源于微信公众号: 张俊发



微信扫描下方的二维码阅读本文

此作者没有提供个人介绍
最后更新于 2023-06-19