SpringBoot实现接口等幂次校验的示例代码

目录
  • 主流的实现方案如下:
  • 第一步:书写redis工具类
  • 第二步、书写token工具类
  • 第三步:定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求
  • 第四步:拦截器配置。选择前置拦截器,每次请求都校验到达的方法上是否有等幂性注解,如果有则进行token校验
  • 第五步:对拦截器进行url模式匹配,并注入spring容器
  • 第六步:控制层

接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功;其目的时防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题。

比如:

  • 订单接口, 不能多次创建订单
  • 支付接口, 重复支付同一笔订单只能扣一次钱
  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调
  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
    等等

主流的实现方案如下:

1、唯一索引:给表加唯一索引,该方法最简单,当数据重复插入时,直接报sql异常,对应用影响不大;

alter table 表名 add unique(字段)

示例,两个字段为唯一索引,如果出现完全一样的order_name,create_time就直接重复报异常;

alter table 'order' add unique(order_name,create_time)

2、先查询后判断:入库时先查询是否有该数据,如果没有则插入。否则不插入;

3、token机制:发起请求的时候先去redis获取token,将获取的token放入请求的hearder,当请求到达服务端的时候拦截请求,对请求的hearder中的token,进行校验,如果校验通过则放开拦截,删除token,否则使用自定义异常返回错误信息。

第一步:书写redis工具类

​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtils {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * 判断key是否存在
     * @param key 键
     * @return
     */
    public boolean hasKey(String key){
        try {
            return redisTemplate.hasKey(key);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除key
     * @param key 键
     * @return
     */
    public Boolean del(String key){
        if (key != null && key.length() > 0){
            return redisTemplate.delete(key);
        }else {
            return false;
        }
    }

    /**
     * 普通缓存获取
     * @param key 键
     * @return
     */
    public Object get(String key){
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return
     */
    public boolean set(String key,Object value,long time){
        try {
            if (time > 0){
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            }
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
}

​

第二步、书写token工具类

import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.CommonException;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
 * 使用uuid生成随机字符串,
 * 通过md5加密防止token被解密,保证token的唯一性与安全性
 * 设置过期时间为30秒,即在30秒内之恶能提交成功一次请求
 */
@Component
public class TokenUtils {

    @Autowired
    RedisUtils redisUtils;

    //token过期时间为30秒
    private final static Long TOKEN_EXPIRE = 30L;

    private final static String TOKEN_NAME = "token";

    /**
     * 生成token放入缓存
     */
    public String generateToken(){
        String uuid = UUID.randomUUID().toString();
        String token = DigestUtils.md5DigestAsHex(uuid.getBytes());
        redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE);
        return token;
    }

    /**
     * token校验
     */
    public boolean verifyToken(HttpServletRequest request){
        String token = request.getHeader(TOKEN_NAME);
        //header中不存在token
        if (StringUtils.isEmpty(token)){
            //抛出自定义异常
            System.out.println("token不存在");
            throw new CommonException(CodeMsg.NOT_TOKEN);
        }
        //缓存中不存在
        if (!redisUtils.hasKey(TOKEN_NAME)){
            System.out.println("token已经过期");
            throw new CommonException(CodeMsg.TIME_OUT_TOKEN);
        }
        String cachToken = (String) redisUtils.get(TOKEN_NAME);
        if (!token.equals(cachToken)){
            System.out.println("token检验失败");
            throw new CommonException(CodeMsg.VALIDA_ERROR_TOKEN);
        }
        //移除token
        Boolean del = redisUtils.del(TOKEN_NAME);
        if (!del){
            System.out.println("token删除失败");
            throw new CommonException(CodeMsg.DEL_ERROR_TOKEN);
        }
        return true;
    }
}

第三步:定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 /**
 * 当控制层的方法上被注释时,表示该请求为等幂性请求
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}

第四步:拦截器配置。选择前置拦截器,每次请求都校验到达的方法上是否有等幂性注解,如果有则进行token校验

import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;
 
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
 
    @Autowired
    private TokenUtils tokenUtils;
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)){
            return true;
        }
        //对有Idempotent注解的方法进行拦截校验
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
        if (methodAnnotation != null){
            //token校验
            tokenUtils.verifyToken(request);
        }
        return true;
    }
 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

第五步:对拦截器进行url模式匹配,并注入spring容器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 对拦截器进行url模式匹配,并注入spring容器
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Autowired
    IdempotentInterceptor idempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截所有请求
        registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**");
    }
}

第六步:控制层

对控制层进行编写,发起请求时通过getToken方法获取token,将获取的token放入hearder后,再请求具体方法。正常请求具体方法的时候注意在hearder中加入token,否则是失败

import com.alibaba.fastjson.JSONObject;
import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.ResultPage;
import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SmileController {

    @Autowired
    TokenUtils tokenUtils;

    @GetMapping("smile/token")
    public ResultPage getToken(){
        String token = tokenUtils.generateToken();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("token",token);
        return ResultPage.success(CodeMsg.SUCCESS,jsonObject);
    }

    @Idempotent
    @GetMapping("smile/test")
    public ResultPage testIdempotent(){
        return ResultPage.success(CodeMsg.SUCCESS,"校验成功");
    }
}

到此这篇关于SpringBoot实现接口等幂次校验的示例代码的文章就介绍到这了,更多相关SpringBoot实现接口等幂次校验 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Boot实现通用的接口参数校验

    本文介绍基于 Spring Boot 和 JDK8 编写一个 AOP ,结合自定义注解实现通用的接口参数校验. 缘由 目前参数校验常用的方法是在实体类上添加注解,但对于不同的方法,所应用的校验规则也是不一样的,例如有一个 AccountVO 实体: public class AccountVO { private String name; // 姓名 private Integer age; // 年龄 } 假设存在这样一个业务:用户注册时需要填写姓名和年龄,用户登陆时只需要填写姓名就可以了.那

  • SpringBoot + validation 接口参数校验的思路详解

    有参数传递的地方都少不了参数校验.在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全.试想一下,如果在controller层中没有经过任何校验的参数通过service层.dao层一路来到了数据库就可能导致严重的后果,最好的结果是查不出数据,严重一点就是报错,如果这些没有被校验的参数中包含了恶意代码,那就可能导致更严重的后果. 实践 一.引入依赖 <!--引入spring-boot-starter-validation--> <dependency> <gr

  • SpringBoot+Redis实现后端接口防重复提交校验的示例

    目录 1 Maven依赖 2 RepeatedlyRequestWrapper 3 RepeatableFilter 4 RepeatSubmit 5 RepeatSubmitInterceptor 6 RepeatSubmitConfig 7 RepeatSubmitController 1 Maven依赖 <!--redis缓存--> <dependency> <groupId>org.springframework.boot</groupId> <

  • SpringBoot实现接口等幂次校验的示例代码

    目录 主流的实现方案如下: 第一步:书写redis工具类 第二步.书写token工具类 第三步:定义注解,使用在方法上,当控制层的方法上被注释时,表示该请求为等幂性请求 第四步:拦截器配置.选择前置拦截器,每次请求都校验到达的方法上是否有等幂性注解,如果有则进行token校验 第五步:对拦截器进行url模式匹配,并注入spring容器 第六步:控制层 接口等幂性通俗的来说就是同一时间内,发起多次请求只有一次请求成功:其目的时防止多次提交,数据重复入库,表单验证网络延迟重复提交等问题. 比如: 订

  • SpringBoot实现接口的各种参数校验的示例

    目录 1.添加依赖 2.接口参数校验 2.1 requestBody参数校验 2.2 requestParam/PathVariable参数校验 3.统一异常处理 4.进阶使用 4.1 分组校验 4.2 嵌套校验 4.3 集合校验 4.4 自定义校验 5.快速失败 (Fail Fast) 6.@Valid和@Validated区别 7.实现原理 7.1 requestBody参数校验实现原理 7.2 方法级别的参数校验实现原理 在我们进行接口开发时,在对参数的接收时,我们需要冗余复杂的校验规则

  • SpringBoot结合JSR303对前端数据进行校验的示例代码

    一.校验分类 数据的校验一般分为**前端校验.后端校验** 二.前端校验 前端校验是最为明显的,先说一下: ① HTML 非空校验 如 HTML5 新增的属性required="true",一旦没有填写就输入框就显示红色,具体使用如: <input type="text" id="name" name="name" required="true"/> ② JS 同时在提交表单发送 Ajax请求

  • SpringBoot中整合Shiro实现权限管理的示例代码

    之前在 SSM 项目中使用过 shiro,发现 shiro 的权限管理做的真不错,但是在 SSM 项目中的配置太繁杂了,于是这次在 SpringBoot 中使用了 shiro,下面一起看看吧 一.简介 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码和会话管理.使用Shiro的易于理解的API,您可以快速.轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序. 三个核心组件: 1.Subject 即"当前操作用户".但是,在 Shi

  • SpringBoot实现阿里云短信发送的示例代码

    阿里云accessID和secret请自行进入阿里云申请 sms.template.code 请进入阿里云,进行短信服务进行魔板添加 开源代码地址在文章末尾 话不多说,直接上代码: application.properties: server.port=8002 #server.servlet.context-path=/ spring.datasource.url=jdbc:mysql://localhost:3306/ssm_message?useUnicode=true&character

  • SpringBoot+WebSocket+Netty实现消息推送的示例代码

    上一篇文章讲了Netty的理论基础,这一篇讲一下Netty在项目中的应用场景之一:消息推送功能,可以满足给所有用户推送,也可以满足给指定某一个用户推送消息,创建的是SpringBoot项目,后台服务端使用Netty技术,前端页面使用WebSocket技术. 大概实现思路: 前端使用webSocket与服务端创建连接的时候,将用户ID传给服务端 服务端将用户ID与channel关联起来存储,同时将channel放入到channel组中 如果需要给所有用户发送消息,直接执行channel组的writ

  • SpringBoot应用整合ELK实现日志收集的示例代码

    ELK即Elasticsearch.Logstash.Kibana,组合起来可以搭建线上日志系统,本文主要讲解使用ELK来收集SpringBoot应用产生的日志. ELK中各个服务的作用 Elasticsearch:用于存储收集到的日志信息: Logstash:用于收集日志,SpringBoot应用整合了Logstash以后会把日志发送给Logstash,Logstash再把日志转发给Elasticsearch: Kibana:通过Web端的可视化界面来查看日志. 使用Docker Compos

  • springboot整合Mybatis、JPA、Redis的示例代码

    引言 在springboot 项目中,我们是用ORM 框架来操作数据库变的非常方便.下面我们分别整合mysql ,spring data jpa 以及redis .让我们感受下快车道. 我们首先创建一个springboot 项目,创建好之后,我们来一步步的实践. 使用mybatis 引入依赖: <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-

  • SpringBoot通过自定义注解实现日志打印的示例代码

    前言 在我们日常的开发过程中通过打印详细的日志信息能够帮助我们很好地去发现开发过程中可能出现的Bug,特别是在开发Controller层的接口时,我们一般会打印出Request请求参数和Response响应结果,但是如果这些打印日志的代码相对而言还是比较重复的,那么我们可以通过什么样的方式来简化日志打印的代码呢? SpringBoot 通过自定义注解实现权限检查可参考我的博客:SpringBoot 通过自定义注解实现权限检查 正文 Spring AOP Spring AOP 即面向切面,是对OO

随机推荐