精华内容
下载资源
问答
  • springboot2.1+redis+拦截器 防止表单重复提交详细完整介绍,所用到的文件都上传了,下载即可使用。自己花了半天整理,并且测试通过,使用在实际项目中的,希望对每一个下载的朋友有帮助。
  • <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=... <modelVersion>...
  • spring防重复token拦截器拦截请求url,判断url对应的controller方法是是否注解有生成防重复token的标识->生成防重复token保存到redis中RedisUtil.getRu().setex(“formToken_” + uuid, “1”, 60 * 60);同时将...

    处理流程

    用户访问表单添加页面->spring防重复token拦截器拦截请求url,判断url对应的controller方法是是否注解有生成防重复token的标识->生成防重复token保存到redis中RedisUtil.getRu().setex(“formToken_” + uuid, “1”, 60 * 60);同时将本次生存的防重复token放到session中->跳转到表单页面时重token中取出放入表单->用户填写完信息提交表单->spring防重复token拦截器拦截请求url,判断提交的url对应controller方法是否注解有处理防重复提交token的标识->redis中formToken做原子减1操作RedisUtil.getRu().decr(“formToken_” + clinetToken);如果不redis中做了减1操作后值不为0,则为重复提交,返回false。

    自定义注解

    为何要用自定义注解

    有些方法我们想要它只能被特定的用户访问到,比如用户登录之后才能访问。spring 的拦截器可以配置拦截的路由,但在 restful 风格的路由中,往往有重复的,根据 http method 来指定功能,这样子的话直接配置拦截器路由规则也不太方便。所以我们可以自定义一个注解,将它用在需要登录的方法中,然后在拦截器中判断要访问的方法是否有我们自定义的注解,如果有就判断当前用户是否登录了(判断是否携带了登录之后获取到的 token ),从而决定是否拦截。

    @Target(ElementType.METHOD)
    
        @Retention(RetentionPolicy.RUNTIME)
    
        public @interface AnnotationToken {
    
            /**
    
             * 需要防重复功能的表单入口URL对应的controller方法需要添加的注解,用于生成token(默认为uuid)
    
             * @return
    
             */
    
            boolean repeat() default false;
    
            /**
    
             * 防重复表单提交表单到后台对应的URL的controller方法需要添加的注解,用于第一次成功提交后remove掉token
    
             * @return
    
             */
    
            boolean checkRepeat() default false;
    
        }
    

    拦截器相关设置

    1. 添加拦截器

       public class TokenInterceptor extends HandlerInterceptorAdapter {
       
                   private static final Logger logger = Logger.getLogger(TokenInterceptor.class);
       
                   @Autowired
       
                   private RedisUtils redisUtils;
       
                   @Override
       
                   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
       
                       //如果没有映射到方法直接通过
       
                       if(!(handler instanceof HandlerMethod)){
       
                           return true;
       
                       }
       
                       HandlerMethod handlerMethod = (HandlerMethod)handler;
       
                       Method method = handlerMethod.getMethod();
       
                       //获取注解
       
                       AnnotationToken annotation = method.getAnnotation(AnnotationToken.class);
       
                       //如果没有注解,直接通过
       
                       if(annotation == null){
       
                           return true;
       
                       }
       
                       //获取注解repeat
       
                       boolean needrepeat = annotation.repeat();
       
                       //如果需要防重发,在redis中添加计数器
       
                       if(needrepeat){
       
                           String uuid = UUID.randomUUID().toString();
       
                           String key = "token" + uuid;
       
                           System.out.println("uuid:"+uuid);
       
                           redisUtils.set("token_"+uuid,"1",60*60);
       
                           request.getSession().setAttribute("token",uuid);
       
                           logger.warn(request.getServletPath() + "---->formToken:" + uuid);
       
                       }
       
                       //获取注解checkRepeat,第一次提交则去掉token,并允许交易通过,重复提交则不允许交易通过
       
                       boolean needCheckRepeat = annotation.checkRepeat();
       
                       if(needCheckRepeat){
       
                           if(isRepeatSubmit(request)){
       
                               logger.warn("please don't repeat submit,url:" + request.getServletPath());
       
                               return false;
       
                           }
       
                       }
       
                       return true;
       
                   }
       
               
       
                   private boolean isRepeatSubmit(HttpServletRequest request){
       
                       String token = (String)request.getSession().getAttribute("token");
       
                       //检查redis中是否存在该用户的token
       
                       boolean r = redisUtils.hasKey("token"+token);
       
                       if(r){
       
                           //用户token减一,如果等于0,则是第一次提交
       
                           Long count = redisUtils.decr("token"+token,1);
       
                           redisUtils.del("token"+token);
       
                           if(count == 0){
       
                               //删除用户token
       
                               return false;
       
                           }
       
                       }
       
                       return true;
       
                   }
       
               }
      
    2. 在spring中注册拦截器

       @Configuration
       
               public class WebAppConfig extends WebMvcConfigurerAdapter {
       
               
       
                   @Override
       
                   public void addInterceptors(InterceptorRegistry registry){
       
                       //注册自定义拦截器
       
                       registry.addInterceptor(tokenInterceptor()).addPathPatterns("/**");    // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
       
                       super.addInterceptors(registry);
       
                   }
       
               
       
                   @Bean
       
                   public TokenInterceptor tokenInterceptor(){
       
                       return new TokenInterceptor();
       
                   }
       
               }
      

    添加Redis相关配置和工具类

    1. 添加redis相关配置Resource/conf/redis.properties

       	redis.host=127.0.0.1
       	
       	redis.port=6379
       	
       	redis.timeout=10000
       	
       	redis.maxIdle=300
       	
       	redis.maxTotal=1000
      
    2. 添加redis工具类

           public class RedisUtils {
       	 @Autowired
      
               private RedisTemplate redisTemplate;
      
           //    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
      
           //        this.redisTemplate = redisTemplate;
      
           //    }
      
               //=============================common============================
      
               /**
      
                * 指定缓存失效时间
      
                * @param key 键
      
                * @param time 时间(秒)
      
                * @return
      
                */
      
               public boolean expire(String key,long time){
      
                   try {
      
                       if(time>0){
      
                           redisTemplate.expire(key, time, TimeUnit.SECONDS);
      
                       }
      
                       return true;
      
                   } catch (Exception e) {
      
                       e.printStackTrace();
      
                       return false;
      
                   }
      
               }
      
           
      
               /**
      
                * 根据key 获取过期时间
      
                * @param key 键 不能为null
      
                * @return 时间(秒) 返回0代表为永久有效
      
                */
      
               public long getExpire(String key){
      
                   return redisTemplate.getExpire(key,TimeUnit.SECONDS);
      
               }
      
           
      
               /**
      
                * 判断key是否存在
      
                * @param key 键
      
                * @return true 存在 false不存在
      
                */
      
               public boolean hasKey(String key){
      
                   try {
      
                       return redisTemplate.hasKey(key);
      
                   } catch (Exception e) {
      
                       e.printStackTrace();
      
                       return false;
      
                   }
      
               }
          }
      
    3. 添加redis的配置类

       @PropertySource("classpath:conf/redis.properties")
       @Configration
       public class RedisConfig {
      
           @Value("${redis.host}")
      
           private String host;
      
           @Value("${redis.port}")
      
           private Integer port;
      
           @Value("${redis.maxTotal}")
      
           private Integer maxTotal;
      
           @Value("${redis.maxIdle}")
      
           private Integer maxIdle;
      
           @Value("${redis.timeout}")
      
           private Integer timeout;
      
       
      
           @Bean
           public JedisPoolConfig jedisPoolConfig(){
      
               JedisPoolConfig config = new JedisPoolConfig();
      
               config.setMaxTotal(maxTotal);
      
               config.setMaxIdle(maxIdle);
      
               return config;
      
           }
      
       
      
           @Bean
           public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig config){
      
               JedisConnectionFactory factory = new JedisConnectionFactory(config);
      
               factory.setHostName(host);
      
               factory.setPort(port);
      
               factory.setTimeout(timeout);
      
               return factory;
      
           }
      
       
      
           @Bean
           public RedisTemplate redisTemplate(JedisConnectionFactory factory){
      
               RedisTemplate redisTemplate = new RedisTemplate();
      
               redisTemplate.setKeySerializer(new StringRedisSerializer());
      
               redisTemplate.setHashKeySerializer(new StringRedisSerializer());
      
               redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
      
               redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      
               // 开启事务
      
               redisTemplate.setEnableTransactionSupport(true);
      
               redisTemplate.setConnectionFactory(factory);
      
               return redisTemplate;
      
           }
       }	
      
    展开全文
  • 利用redis+token+拦截器+注解(只在需要防止重复的接口上添加该注解即可)实现防止重复订单.


    项目地址:

    利用redis+token+拦截器+注解(只在需要防止重复的接口上添加该注解即可)实现防止重复订单,在跳转到提交订单的页面时就通过getNotRepeatToken获取到本次订单的唯一标识(以业务加上用户id加上uuid作为key)存入redis,并返回给前端。在提交订单时,将该标识设置在请求头中,第一次提交订单时,在redis中删除该key。后续再次提交订单时redis中已经不存在该key,则给出重复提交订单的友好提示。
    springboot 拦截器注入service为null的问题,可以通过bean工厂来完成手动注入。

    定义注解和拦截器

    注解:NotRepeat.java

    package com.demo.aop;
    
    import java.lang.annotation.*;
    
    /**
     * 禁止重复提交的注解
     * @Author: zlw
     * @Date: 2019/7/16 10:55
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface NotRepeat {
    }
    
    

    拦截器:NotRepeatInterceptor.java

    package com.demo.config;
    
    import com.demo.aop.NotRepeat;
    import com.demo.enums.ExceptionEnum;
    import com.demo.exception.ServiceException;
    import com.demo.utils.RedisUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.context.support.WebApplicationContextUtils;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * @Author: zlw
     * @Date: 2019/7/16 11:09
     */
    @Slf4j
    public class NotRepeatInterceptor implements HandlerInterceptor {
    
    
        @Autowired
        private RedisUtils redisUtils;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //解决service为null   因为拦截器是在springContext之前加载的,通过@component注解的bean还没有注入到容器中
            if (redisUtils == null) {
                BeanFactory factory= WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
                 redisUtils = (RedisUtils) factory.getBean("redisUtils");
            }
    
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
    
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            NotRepeat annotation = handlerMethod.getMethodAnnotation(NotRepeat.class);
            //如果存在该注解,则进行幂等性校验,若校验不通过则抛出自定义异常
            if (annotation != null) {
                checkNotRepeat(request);
            }
            return true;
        }
    
        /**
         * 重复校验
         * @param request
         *
         *
         * 注意: 在删除key时需要判断是否删除成功,即redisUtils.deleteKey()的返回值是否小于1。
         *      若删除不成功则视为重复提交,防止网络延迟等故障时,两次请求
         *          在redisUtils.hasKey(notRepeat)时都为true,但是还没有进行到删除步骤(删除操作只能删除一次,不存在多次删除)
         */
        private void checkNotRepeat(HttpServletRequest request) throws ServiceException {
            String notRepeat = request.getHeader("notRepeat");
            //校验请求头中是否携带了notRepeat的key
            if (StringUtils.isBlank(notRepeat)) {
                throw new ServiceException(ExceptionEnum.BAD_PARAM);
            }
            //校验是否存在该key
            if (!redisUtils.hasKey(notRepeat)) {
                throw new ServiceException(ExceptionEnum.REPEAT_REQUEST);
            }
            //校验是否删除成功
            if ((redisUtils.deleteKey(notRepeat) < 1)) {
                throw new ServiceException(ExceptionEnum.SERVER_ERROR);
            }
        }
    
    
    }
    
    

    请求头中添加参数的方法:

    $.ajax({
        url: prefix + '/addBill',
        type: 'POST',
        dataType: "json",
        headers:{"notRepeat":"NOT_REPEAT:13135541171:67fc4069-5041-4e35-91a0-8b850aff9ea5"}
        
        })
    

    工具类

    package com.demo.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author zlw
     * Date : 2019/7/16
     * Description : Redis工具
     */
    @Component
    @Slf4j
    public class RedisUtils {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
    
        /**
         * 设置redis String类型数据
         * @param key
         * @param value
         */
        public void setStringKeyValue(String key,String value,Long timeout) {
            if (timeout > 0) {
                stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
            }else {
                stringRedisTemplate.opsForValue().set(key, value);
            }
        }
    
        /**
         * 根据key获取redis String类型的value
         * @param key
         * @return
         */
        public String getStringKeyValue(String key) {
            return stringRedisTemplate.opsForValue().get(key);
        }
    
    
    
        /**
         * 删除key   
         * @param key
         */
        public Long deleteKey(String key) {
          return stringRedisTemplate.execute(connection -> connection.del(key.getBytes()), true);
        }
    
        /**
         * 删除key  
         * @param key
         */
        public Long deleteKeys(String ...key) {
            return stringRedisTemplate.delete(Arrays.asList(key));
        }
    
        /**
         * 获得过期时间  默认秒
         * @param key
         * @return
         */
        public Long getExpireTime(String key) {
            return stringRedisTemplate.getExpire(key);
        }
    
    
        public Boolean hasKey(String key) {
            try {
                return stringRedisTemplate.hasKey(key);
            } catch (Exception e) {
                log.info("msg:{}",e.getMessage());
                return false;
            }
        }
    
    }
    
    

    配置类:MyMvcConfig.java

    package com.demo.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @Author: zlw
     * @Date: 2019/7/16 13:37
     */
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new NotRepeatInterceptor()).addPathPatterns("/**");
        }
    }
    
    

    其他自定义异常和枚举类等可以在项目中查看或自定义。

    测试类

    TestController.java

    package com.demo.controller;
    
    import com.demo.aop.NotRepeat;
    import com.demo.pojo.User;
    import com.demo.service.ITestService;
    import com.demo.utils.RedisUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.UUID;
    
    /**
     * @author zlw
     * @date 2019/7/16
     */
    @RestController
    @RequestMapping("test")
    @Slf4j
    public class TestController {
    
        private static final String NOT_REPEAT_PREFIX = "NOT_REPEAT";
    
        @Autowired
        private ITestService testService;
    
    
        @Autowired
        private RedisUtils redisUtils;
    
        /**
         * 通过在mapper.xml中写sql进行查询
         * @param username
         * @return
         */
        @GetMapping("userByUsername")
        public ResponseEntity<User> getUserByUsername(@RequestParam("username") String username){
            User user = testService.getUserByUsername(username);
            return ResponseEntity.ok(user);
        }
    
        /**
         * 通过通用mapper自带的方法进行查询
         * @param id
         * @return
         */
        @NotRepeat
        @GetMapping("userById2")
        public ResponseEntity<User> getUserById(@RequestParam("id") Integer id){
            User user = testService.getUserById(id);
            return ResponseEntity.ok(user);
        }
    
        /**
         * 获取重复提交标识
         * @param key
         * @return
         */
        @GetMapping("getNotRepeatToken")
        public ResponseEntity<String> getToken(@RequestParam("key") String key) {
            UUID uuid = UUID.randomUUID();
            StringBuilder str = new StringBuilder();
            String newKey = str.append(NOT_REPEAT_PREFIX).append(":" + key).append(":" + uuid).toString();
            redisUtils.setStringKeyValue(newKey,"1",30L);
            return ResponseEntity.ok(newKey);
        }
    
    
    
    
    }
    
    

    常见异常

    • 拦截器中无法注入service,利用BeanFactory手动注入service
    java.lang.NullPointerException: null
    	at com.demo.config.NotRepeatInterceptor.checkNotRepeat(NotRepeatInterceptor.java:63) ~[classes/:na]
    	at com.demo.config.NotRepeatInterceptor.preHandle(NotRepeatInterceptor.java:43) ~[classes/:na]
    	at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:136) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:986) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) ~[tomcat-embed-core-8.5.34.jar:8.5.34]
    	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
    	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[tomcat-embed-core-8.5.34.jar:8.5.34]
    
    展开全文
  • 按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下方式: 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据 token机制,每次接口请求前先获取...

    一:在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同 。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下方式:

    1. 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
    2. token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token
    3. 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
    4. 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
      在这里插入图片描述
      根据上图搭建相关服务
    5. redis api提供 springBoot项目pom引入依赖
    <!-- 实现对 Spring Data Redis 的自动化配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    
    /**
     * redis工具类
     */
    @Component
    public class RedisService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 写入缓存
         * @param key
         * @param value
         * @return
         */
        public boolean set(final String key, Object value) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
    
        /**
         * 写入缓存设置时效时间
         * @param key
         * @param value
         * @return
         */
        public boolean setEx(final String key, Object value, Long expireTime) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
    
        /**
         * 判断缓存中是否有对应的value
         * @param key
         * @return
         */
        public boolean exists(final String key) {
            return redisTemplate.hasKey(key);
        }
    
        /**
         * 读取缓存
         * @param key
         * @return
         */
        public Object get(final String key) {
            Object result = null;
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            result = operations.get(key);
            return result;
        }
    
        /**
         * 删除对应的value
         * @param key
         */
        public boolean remove(final String key) {
            if (exists(key)) {
                Boolean delete = redisTemplate.delete(key);
                return delete;
            }
            return false;
    
        }
    
    }
    

    二: 自定义注解AutoIdempotent
    自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时

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

    三:token创建和检验
    1:token服务接口

    我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端

    ```java
    public interface TokenService {
    
        /**
         * 创建token
         * @return
         */
        public  String createToken();
    
        /**
         * 检验token
         * @param request
         * @return
         */
        public boolean checkToken(HttpServletRequest request) throws Exception;
    }
    

    2:token的服务实现类 token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。

    `@Service
    public class TokenServiceImpl implements TokenService {
    
        @Autowired
        private RedisService redisService;
    
    
        /**
         * 创建token
         *
         * @return
         */
        @Override
        public String createToken() {
            String str = RandomUtil.randomUUID();
            StrBuilder token = new StrBuilder();
            try {
                token.append(Constant.Redis.TOKEN_PREFIX).append(str);
                redisService.setEx(token.toString(), token.toString(),10000L);
                boolean notEmpty = StrUtil.isNotEmpty(token.toString());
                if (notEmpty) {
                    return token.toString();
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }
            return null;
        }
    
    
        /**
         * 检验token
         *
         * @param request
         * @return
         */
        @Override
        public boolean checkToken(HttpServletRequest request) throws Exception {
    
            String token = request.getHeader(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// header中不存在token
                token = request.getParameter(Constant.TOKEN_NAME);
                if (StrUtil.isBlank(token)) {// parameter中也不存在token
                    throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
                }
            }
    
            if (!redisService.exists(token)) {
                throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
            }
    
            boolean remove = redisService.remove(token);
            if (!remove) {
                throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
            }
            return true;
        }
    }``
    

    四、拦截器的配置
    1:web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中

    @Configuration
    public class WebConfiguration extends WebMvcConfigurerAdapter {
    
        @Resource
       private AutoIdempotentInterceptor autoIdempotentInterceptor;
    
        /**
         * 添加拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(autoIdempotentInterceptor);
            super.addInterceptors(registry);
        }
    }
    

    2:拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端

    /**
     * 拦截器
     */
    @Component
    public class AutoIdempotentInterceptor implements HandlerInterceptor {
    
        @Autowired
        private TokenService tokenService;
    
        /**
         * 预处理
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            //被ApiIdempotment标记的扫描
            AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
            if (methodAnnotation != null) {
                try {
                    return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
                }catch (Exception ex){
                    ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
                    writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
                    throw ex;
                }
            }
            //必须返回true,否则会被拦截一切请求
            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 {
    
        }
    
        /**
         * 返回的json值
         * @param response
         * @param json
         * @throws Exception
         */
        private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
            PrintWriter writer = null;
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html; charset=utf-8");
            try {
                writer = response.getWriter();
                writer.print(json);
    
            } catch (IOException e) {
            } finally {
                if (writer != null)
                    writer.close();
            }
        }
    
    }
    

    五、总结

    本篇博客介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。

    展开全文
  • 在开发过程中经常需要做防止重复提交处理,例如:下订单,保存信息等等 前端处理思路: 点击按钮后,立即将按钮置灰且不可使用,然后调用处理逻辑接口,当接口有响应后重新使按钮重新亮起可用 后端处理思路: ...

    【简介】

    在开发过程中经常需要做防止重复提交处理,例如:下订单,保存信息等等

    • 前端处理思路:
      点击按钮后,立即将按钮置灰且不可使用,然后调用处理逻辑接口,当接口有响应后重新使按钮重新亮起可用

    • 后端处理思路:
      思路一、建立数据库唯一索引,通过数据库唯一索引,保证数据唯一
      思路二、通过token方式,调用业务接口前先调用接口获取token,调用业务接口时传入token,先进行token校验和处理,当token正确时删除该token(第二次传入相同token就会校验不通过),然后处理正常的业务逻辑


    【源码GitHub地址】

    GitHub地址

    本文token生成后存入Redis,所以在Redis构建的项目基础上进行处理的,Redis项目构建文章:
    SprinBoot2.2.x 整合 Redis


    【项目结构】

    在这里插入图片描述


    【项目搭建】

    【pom.xml】

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.13.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <groupId>com.example</groupId>
        <artifactId>springboot2.2.x_redis</artifactId>
        <version>1.0.0</version>
        <name>springboot2.2.x_redis</name>
        <description>SpringBoot2.2.x demo project for Redis</description>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!-- redis依赖commons-pool 这个依赖一定要添加 -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
    
            <!-- 支持 @ConfigurationProperties 注解 读取properties或yml配置文件中的值 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    【ActionToken】

    用于生成和删除token处理

    package com.example.springboot_redis.token;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 生成token和删除token
     */
    @Slf4j
    @Component
    public class ActionToken {
    
        // 默认缓存时间
        private final Long TOKEN_EXPIRE_TIME = 60 * 60 * 24L;
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        /*
         * 生成token
         */
        public String createToken(String sessionId) {
            if (!StringUtils.hasText(sessionId)) {
                return null;
            }
    
            // 使用UUID当token
            String token = UUID.randomUUID().toString();
            // 存入缓存并设置有效期 TimeUnit.SECONDS 单位:秒
            stringRedisTemplate.opsForValue().set(token, sessionId, TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
    
            if (!StringUtils.hasText(stringRedisTemplate.opsForValue().get(token))) {
                throw new RuntimeException("生成token缓存redis失败");
            }
    
            return token;
        }
    
        /*
         * 校验token
         */
        public String tokenVerify(String token) {
            if (!StringUtils.hasText(token)) {
                log.info("token 为空");
                return "请勿重复提交";
            }
    
            String sessionId = stringRedisTemplate.opsForValue().get(token);
            if (!StringUtils.hasText(sessionId)) {
                log.info("Redis 中 key 为 token 的不存在");
                return "请勿重复提交";
            }
    
            // token 存在,移除Redis中的token,进入业务逻辑
            stringRedisTemplate.delete(token);
            log.info("redis 删除key为token:[{}]成功,进入业务逻辑", token);
    
            return "";
        }
    }
    

    【TokenVerify】

    防重复提交注解

    package com.example.springboot_redis.token;
    
    import java.lang.annotation.*;
    
    /*
     * 防重复提交注解
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface TokenVerify {
        String value() default "";
    }
    

    【TokenAspect】

    处理防重复提交的切面

    package com.example.springboot_redis.token;
    
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;
    
    /**
     * 处理防重复提交token切面
     */
    @Slf4j
    @Aspect
    @Component
    public class TokenAspect {
    
        @Autowired
        private ActionToken actionToken;
    
        @Autowired
        private HttpServletRequest request;
    
        @Autowired
        private HttpServletResponse response;
    
        // 切入点签名
        @Pointcut("@annotation(com.example.springboot_redis.token.TokenVerify)")
        private void tokenPoint() {
        }
    
        // 环绕通知
        @Around(value = "tokenPoint()")
        public Object tokenVerify(ProceedingJoinPoint joinPoint) throws Throwable {
    
            String token = request.getHeader("token");
            String msg = actionToken.tokenVerify(token);
            PrintWriter printWriter = null;
    
            if (!StringUtils.hasText(msg)) {
                // 删除token成功,进入业务逻辑
                try {
                    return joinPoint.proceed();
                } catch (Exception e) {
                    log.error("删除token成功,业务处理异常:", e);
                }
            } else {
                try {
                    response.setCharacterEncoding("utf-8");
                    response.setContentType("text/html;charset=utf-8");
                    printWriter = response.getWriter();
                    printWriter.write(msg);
                    printWriter.flush();
                } catch (Exception e) {
                    log.error("处理token,返回错误信息时异常", e);
                } finally {
                    if (printWriter != null) {
                        printWriter.close();
                    }
                }
            }
    
            return null;
        }
    
    }
    

    【TokenController】

    获取token接口和使用@TokenVerify防止重复提交

    package com.example.springboot_redis.contoller;
    
    import com.example.springboot_redis.token.ActionToken;
    import com.example.springboot_redis.token.TokenVerify;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @Slf4j
    @RestController
    @RequestMapping("/token")
    public class TokenController {
    
        @Autowired
        private ActionToken actionToken;
    
        @GetMapping
        public String createToke() {
            return actionToken.createToken("123456");
        }
    
        @TokenVerify
        @PostMapping
        public void business() {
            log.info("正常业务逻辑");
        }
    }
    

    【测试】

    1、 获取token
    http://127.0.0.1/token/
    在这里插入图片描述
    Redis
    在这里插入图片描述
    2、调用测试@TokenVerify方法

    第一次访问:
    在这里插入图片描述
    控制台日志
    在这里插入图片描述
    Redis中的数据已经删除

    第二次访问:
    在这里插入图片描述
    在这里插入图片描述
    OK,大功告成!成功的防止重复调用


    【推荐好文】

    Spring Boot+Redis+拦截器+自定义Annotation实现接口自动幂等

    展开全文
  • 分布式防止重复提交
  • 文章目录前言装备Core-Code新增注解@...SpringBoot防止表单重复提交。基于拦截器对带注解的请求进行拦截,处理。 后面总结一下为什么要如此使用。 应用场景: 使用浏览器后退按钮重复之前的操作...
  • SpringBoot/Web项目防止表单/请求重复提交(单机和分布式) 一、场景/方案 说起web项目的防止表单/请求重复提交,不得不说幂等性。 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。...
  • 按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段: 1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。 2、token机制,每次接口请求前...
  • 一、概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 ... token机制 -- 防止页面重复提交 悲观锁 -- 获取数据的时候加锁(锁表或锁行)...
  • 来源:cnblogs.com/cjsblog/p/14516909.html概述为了防止掉单,这里可以这样处理:为了防止订单重复提交,可以这样处理:附上微信支付最佳实践:概述如图是一个简化...
  • returnMsg.setFail("请勿重复提交"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = null; try { out = response.getWriter(); ...
  • 点击上方“朱小厮的博客”,选择“设为星标”后台回复"书",获取后台回复“k8s”,可领取k8s资料概述如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付...
  • 去除一些可变字段,将key升序排序,拼接成字符串并进行md5加密,再拼接一些用户信息,这样相同的请求参数得到的加密串必然一致,将此字符串作为key,存入redis,设置过期时间为1秒,一般重复提交都是在1000ms以内;...
  • 在系统中,有些接口如果重复提交,可能会造成脏数据或者其他的严重的问题,所以我们一般会对与数据库有交互的接口进行重复处理。我们首先会想到在前端做一层控制。当前端触发操作时,或弹出确认界面,或disable入口...
  • 表单重复提交是在web中存在的一个很常见,会带来很多麻烦的一个问题。尤其是在表单新增的时候,如果重复提交了多条一样的数据,带来的麻烦更大。 实现防止表单重复提交的方法有前端限制和后台限制1、前端限制就是当...
  • 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多次回调, 必须处理重复回调 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次 等等 二、常见解决方案 唯一索引 ...
  • 这个 项目是我给某个客户做的一个定制化微信小程序商城项目,使用 spring boot+redis+mysql开发 ,前端小程序需要用户配置为自己的
  • Jmeter+Springboot+Redisson分布式锁并发订单操作(下单、取消单、完成单、加库存) 涉及知识点: java+springboot+mybatis开发 redis分布式锁+Redisson客户端 Jmeter各种骚操作:用户变量、随机取值、jdbc操作、if ...
  • 来源:cnblogs.com/cjsblog/p/14516909.html如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三...
  • SpringBoot项目实现高并发商品秒杀

    千次阅读 2019-11-18 14:29:59
    SpringBoot项目实现高并发商品秒杀 注:该项目使用IDEA+SpringBoot+Maven+Mybatis+Redis+RabbitMQ 等技术实现。本人水平有限,以下代码可能有错误,或者解释不清,希望理解,并且及时下方留言,及时修改,谢谢各位...
  • 订单防止重复提交 如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付...
  • 这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。 3)在session中存放一个特殊标志 在服务器端,生成一个唯一的标识符,将它存入session,...
  • 点击上方IT牧场,选择置顶或者星标技术干货每日送达关于防重复提交由于本人从事电商开发工作,项目中面对C端用户或多或少都会接触到提交保存或者修改的请求,例如创建订单,物流包裹签收...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,723
精华内容 1,089
关键字:

springboot订单重复提交

spring 订阅