SpringBoot项目中使用Redis+注解+拦截器实现接口幂等性校验

一、概念

1. 幂等性定义

  幂等性原本是数学上的概念,公式:f(x)=f(f(x)) 能够成立的数学性质。用在编程领域,则意为对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。举个简单例子来说,就是我们在添加一个学生信息的时候,由于某种原因(网络抖动之类),导致发送多次请求,只能保存一次提交的信息。

2. 幂等性需注意的问题

  • 幂等性的实质是一次或多次请求同一个资源,其结果是相同的。其关注的是对资源产生的影响(副作用)而不是结果,结果可以不同
  • 网络超时、服务宕机等问题,不是幂等的范围

3. 重复提交和幂等对比

  • 重复提交:重复提交是在第一次请求成功的情况下,人为的进行多次操作,从而导致不满足幂等性要求的服务多次改变数据状态。
  • 幂等:更多使用的情况是第一次请求知道结果(比如常见的网络抖动导致连接超时)或者失败异常情况下,发起多次请求的,其目的是多次确认第一次请求成功,却不会因为多次请求而出现多次的状态变化。

二、实现思路

  • 前端发送请求前获取token,将token存入redis中
  • 请求接口时携带token,接口添加注解
  • 后端拦截器拦截本次请求,判断redis中是否有此token
  • token存在,正常执行,token不存在,属于重复提交,参数中未携带token,终止操作。

三、代码具体实现

  项目所用技术:SpringBoot+redis,导包以及redis工具类此处省略,只写校验流程,以免篇幅过长。

  接口幂等性拦截器

/**
 * @author lqh
 * @date 2020/9/21
 * 接口幂等性拦截器
 */
@Component
public class IdempotenceInterceptor implements HandlerInterceptor{

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod= (HandlerMethod) handler;
        Method method=handlerMethod.getMethod();
        Idempotence methodIdempotence=method.getAnnotation(Idempotence.class);
        if(methodIdempotence != null){
            // 幂等性校验, 校验通过则放行, 校验失败则抛出异常(自定义异常,返回重复提交信息)
            check(request);
        }
        return true;
    }
    private void check(HttpServletRequest request) {
        tokenService.checkToken(request);
    }

}

  注册拦截器

/**
 * @author lqh
 * @date 2020/9/21
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private IdempotenceInterceptor idempotenceInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(idempotenceInterceptor);
    }
}

  自定义注解

/**
 * @author lqh
 * @date 2020/9/21
 *  * 自定义注解,在需要保证幂等性接口的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotence {
}

  业务处理接口

public interface TokenService {
    /**
     * 生成token
     * @return
     */
     ServerResponse createToken();

    /**
     * 校验token
     * @param request
     */
     void checkToken(HttpServletRequest request);
}

  业务处理接口实现类(重点)

@Service("TokenService")
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME="token";
    @Autowired
    private RedisTemplate businessTemplate;
    @Override
    public ServerResponse createToken() {
        String str= RandomUtil.UUID32();
        StrBuilder token=new StrBuilder();
        token.append(Constant.TOKEN_PREFIX).append(str);
        RedisUtil.set(token.toString(),token.toString(),300,businessTemplate);
        return ServerResponse.success(token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token=request.getHeader(TOKEN_NAME);
        if(StringUtils.isBlank(token)){
            token=request.getParameter(TOKEN_NAME);
            if(StringUtils.isBlank(token)){
                throw new ServiceException(ResponseCodeEnum.ILLEGAL_ARGUMENT.getMsg());
            }
        }
        if(!RedisUtil.hasKey(token,businessTemplate)){
            throw new ServiceException(ResponseCodeEnum.REPETITIVE_OPERATION.getMsg());
        }
        if(!RedisUtil.del(businessTemplate,token)){
            throw new ServiceException(ResponseCodeEnum.REPETITIVE_OPERATION.getMsg());
        }
    }
}

  校验方法中的删除token一定校验是否删除成功,不能采用直接删除token的方式(如果多个线程同时操作到删除这个地方,不校验的话,会出现并发问题,仍然会有重复提交操作的发生)

  Controller获取token

@RestController
@RequestMapping("/token")
public class TokenController {
    @Autowired
    private TokenService tokenService;

    @RequestMapping("/getToken")
    public ServerResponse token() {
        return tokenService.createToken();
    }

/*    @Idempotence
    @RequestMapping("/testToken")
    public ServerResponse testToken() {
        return ServerResponse.success("测试接口");
    }*/
}

  自定义异常

public class ServiceException extends RuntimeException{

    private String code;
    private String msg;

    public ServiceException() {
    }
    public ServiceException(String msg) {
        this.msg = msg;
    }
    public ServiceException(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

  枚举类响应状态

public enum ResponseCodeEnum {

    // 系统模块
    SUCCESS(200, "操作成功"),
    SAVE_SUCCESS(201,"保存成功"),
    DELETE_SUCCESS(202,"删除成功!"),
    UPDATE_SUCCESS(403,"更新成功!"),
    ERROR(400, "操作失败"),
    SAVE_ERROR(401,"保存失败"),
    DELETE_ERROR(402,"删除失败!"),
    UPDATE_ERROR(403,"更新成功"),
    SERVER_ERROR(500, "服务器异常"),
    EXCEPTION(-1,"Exception"),
    
    // 通用模块 1xxxx
    ILLEGAL_ARGUMENT(10000, "参数不合法"),
    REPETITIVE_OPERATION(10001, "请勿重复操作"),
    ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),
    MAIL_SEND_SUCCESS(10003, "邮件发送成功"),
    PARAMETER_NOT_EMPTY(10004,"参数不能为空"),
    GRMMAR_RULES_ILLEGAL(10005,"语法规则有误,请检查!"),


    //**模块
    ;
    ResponseCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    private Integer code;
    private String msg;
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

四、前端实践方案说明

  这里采用更新操作说明,假设你要修改学生的基本信息。

  • 修改信息的Controller接口添加@Idempotence注解
  • 调出修改信息对话框的同时获取token
  • 提交信息时,将token放入headers里面或者参数中即可

   按我上面说的案例,采用token机制实现幂等性,有一个问题就是,在我获取token时存入redis的时间是五分钟,假如我六分钟之后再次提交,就会提示重复操作。对于这个问题,我还没有想到最优的方案,欢迎各位评论区指导,感谢。

你可能感兴趣的:(工作常用,SpringBoot,token,java,redis,spring)