springboot 实现接口幂等

185 阅读2分钟

springboot 实现接口幂等

1、什么是接口幂等性?

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

接口就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的。

简单理解:就是针对一个操作,不管做多少次,产生的效果都是一样的,常见于表单的重复提交

2、怎么解决这个问题?

使用token机制,在用户提交表单的时候,顺带提交一个从后台获取的token值,当后台第一次处理结束后,删除该token值,那么下次再过来请求的时候,会提示该token失效,继而避免重复操作。

3、具体实现

1、 首先定义一个注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

2、然后定义一个拦截器,用于拦截校验

package cn.couldme.study.web.security.interceptor;
​
import cn.couldme.study.web.common.exception.BizException;
import cn.couldme.study.web.security.annotation.ApiIdempotent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
​
/**
 * 接口幂等性拦截器
 * @Author: szwei
 * @Date: 2020-03-31 15:12
 **/
public class ApiIdempotentInterceptor implements HandlerInterceptor {
​
    @Autowired
    private TokenService tokenService;
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws BizException {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
​
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
​
        ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null) {
            check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
        }
​
        return true;
    }
​
    private void check(HttpServletRequest request) throws BizException {
        tokenService.checkToken(request);
    }
​
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }
​
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}
​

这里调用了 TokenService类的方法,具体实现如下:

public void checkToken(HttpServletRequest request) throws BizException {
        String token = request.getHeader(TOKEN_NAME);
        if (StrUtil.isBlank(token)) {// header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// parameter中也不存在token
                throw new BizException("非法请求");
            }
        }
​
        if (Objects.isNull(redisTemplate.opsForValue().get(token))) {
            throw new BizException("不可以多次提交");
        }
​
        Boolean del = redisTemplate.delete(token);
        if (!del) {
            throw new BizException("不可以多次提交");
        }
    }

从请求头中获取token,如果请求头中没有,从请求参数中获取,如果都获取不到,返回错误。如果获取到了,从Redis中判断是否有这个token,没有说明之前请求过了,返回错误,如果有,执行删除,如果删除失败,返回错误。

3、将拦截器注入

package cn.couldme.study.web.security.interceptor;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
/**
 * 拦截器配置类
 * @Author: szwei
 * @Date: 2020-03-31 16:02
 **/
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
​
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor());
    }
​
​
    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor() {
        return new ApiIdempotentInterceptor();
    }
​
}
​

4、获取token

用户每次打开表单时,先从后台获取token,后台UUID生成token,存到Redis中,等用户提交表单时,校验其token,如果校验成功,执行操作,删除掉token。