实际项目或者面试的时候经常会问到如何防止接口幂等性,故此对本人接触过的方法进行总结。
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();
}