Bootstrap

接口幂等性问题

实际项目或者面试的时候经常会问到如何防止接口幂等性,故此对本人接触过的方法进行总结。

1.使用redis进行延时拦截
让前端传送必要信息以及时间戳然后存入redis,利用redis的超时机制来拦截重复请求,这个方法也是我问了好几个人之后从他们那里得到的方案,但是经认证该方法是有问题的,发现还是会出现重复传送的情况,因此该方案在项目中没有使用

2.从业务上进行拦截
指的是在一定的时间内或者是在某些条件之下,只允许发送一次修改或者新增的请求,例如购买虚拟货币,在平台没有发配货币之前不允许再进行购买,这个可以通过状态判断来决定是否允许重复操作,或者是在一定的时间内只允许操作一次,这个可以通过redis的超时机制+操作人唯一id+操作类型来限制,这种方案在实际使用中是可行的,即使存在前端网络抖动多次操作,也只会有一个操作成功,但是仅适用于特性的业务场景

3.临时令牌的方式
对于由于前端或者网络原因导致的多次请求,我们可以在请求操作接口之前让前端请求一个一次性的令牌,当发出多次请求的时候,如果接口上带的令牌都是一个令牌,那么仅会有一个操作成功,这样可以防止网络或者前端多次触发请求导致的重复操作问题,但是这个方式无法应对”请求token–请求操作接口“捆绑操作的重发问题,目前在实际项目中使用该方式没有重复下单的问题。

以下为方案代码:

/**
 *          防重复提交的注解
 *          放在Controller类:表示当前类的所有接口需要考虑接口幂等性
 *          放在方法上:表示当前方法需要考虑接口幂等性
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatLimiter {
}

注解的拦截

@Component
@Slf4j
public class TokenInterceptor extends HandlerInterceptorAdapter {

    @Value("${jwt.secret}")
    private String secret;

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 获取方法上的注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        PenguinLogThreadLocal.remove();

        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("P3P", "CP=CAO PSA OUR");
        if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) {
            response.addHeader("Access-Control-Allow-Methods", "POST,GET,TRACE,OPTIONS");
            response.addHeader("Access-Control-Allow-Headers", "Content-Type,Origin,Accept");
            response.addHeader("Access-Control-Max-Age", "120");
        }
        
        if (method.isAnnotationPresent(NoVerificationToken.class)) {
            return true;
        }
        
        //校验用户的合法性
        String token = request.getHeader("token");
        response.setContentType("text/javascript;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        Outcome<Long> rspBean = GameTokenUtil.checkToken(token,secret);
        PenguinLogThreadLocal.appendLog("tokenResult",rspBean);
        if (!rspBean.ok()) {
            setOutCome(request,response,rspBean,"401");
            return false;
        }
        UserInfoThreadlocal.put(rspBean.getData());
        PenguinLogThreadLocal.setUserId(rspBean.getData().toString());
        //检验用户合法性
        rspBean = tokenService.checkUser(UserInfoThreadlocal.get());

        if (!rspBean.ok()) {
            setOutCome(request,response,rspBean,"401");
            return false;
        }
		//验证接口是否需要考虑幂等性问题
        if(method.isAnnotationPresent(RepeatLimiter.class)){
            String repeatLimNo = request.getHeader("repeatLimNo");
            Outcome<Long> rspReapBean =tokenService.checkRepeatLimNo(repeatLimNo);
            if(!rspReapBean.ok()){
                setOutCome(request,response,rspReapBean,"402");
                return false;
            }
        }

        return true;
    }

    private PrintWriter setOutCome(HttpServletRequest request,HttpServletResponse response,Outcome<Long> rspBean,String code){
        PrintWriter out = null;
        try {
            JSONObject res = new JSONObject();
            res.put("code",new Long(code));
            res.put("message",rspBean.getMessage()+"");
            res.put("data","");
            PenguinLogThreadLocal.setSuccessful(false);
            out = response.getWriter();
            out.append(res.toString());
            return out;
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return out;
    }

}
    @PostMapping("/repeatLimNo")
    @ApiOperation(value = "获取请求序列", notes = "获取请求序列--拦截重复请求")
    public Outcome<String> getToken(@RequestHeader(value = "token") String token, HttpServletRequest request) {
        return Outcome.success("", repeatLimiterSeq.generateSeq());
    }

token生成

//生成Token
    @Override
    public String generateSeq() {
        //使用UUID生成 token
        String token = UUID.randomUUID().toString();
        token=token.replaceAll("-","");
        //存入Redis,key:token,过期时间10分钟
        redisUtil.set("repeatLimNo::"+token,token,20*60);
        PenguinLogThreadLocal.setParam(token);
        return token;
    }

    @Override
    public Boolean deleteSeq(String repeatLimNo) {
        return stringRedisTemplate.delete("repeatLimNo::"+repeatLimNo);
    }

    @Override
    public String getSeq(String repeatLimNo) {
        Object temp=redisUtil.get("repeatLimNo::"+repeatLimNo);
        if(temp!=null){
            return temp.toString();
        }
        return null;
    }

后端使用代码:

@ApiOperation(value = "测试序列号", notes = "测试序列号")
@PostMapping(value = "测试序列号")
@RepeatLimiter
public Outcome testLimiter(@RequestHeader(value = "token") String token, @RequestHeader(value = "repeatLimNo") String repeatLimNo) {
    return Outcome.success();
}
;