SpingBoot接口防刷

注解+过滤器+拦截器+redis缓存(若依框架)

实现思路:

1.首先过滤器过滤http请求,重新组装为可重复读取的request流(由于需要从request流中读取body数据,而request流不能重复读取,所以需要创建一个可重复读取的流)

2.拦截器拦截到注解标记的指定方法,获取方法请求url以及请求头组成一个缓存键,将请求时间和请求参数放到一个map中作为缓存值。

3.根据缓存键获取缓存中对象,如果存在,判断当前请求参数和上次请求参数是否相同,以及当前请求时间和上次请求时间相差是否在指定范围内,根据规则判断是否重复提交,如果是重复提交,直接返回错误信息。

4.如果不是重复提交,添加缓存键以及值到redis内存当中,用于下一次校验重复提交。

具体实现:

1.创建几个工具类

redis工具类:用于设置和获取缓存

/**
 * spring redis 工具类
 *
 **/
@Component
public class RedisCache
{

    @Resource
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public  T getCacheObject(final String key)
    {
        ValueOperations operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
    

}

HTTP封装工具类:用于获取流中body数据 

/**
 * 通用http工具封装工具
 *
 */
public class HttpHelper
{
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);

    public static String getBodyString(ServletRequest request)
    {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;
        try (InputStream inputStream = request.getInputStream())
        {
            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String line = "";
            while ((line = reader.readLine()) != null)
            {
                sb.append(line);
            }
        }
        catch (IOException e)
        {
            LOGGER.warn("getBodyString出现问题!");
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                }
                catch (IOException e)
                {
                    LOGGER.error(ExceptionUtils.getMessage(e));
                }
            }
        }
        return sb.toString();
    }
}

2.创建一个方法注解,用于拦截指定方法

@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    int interval() default 5000;

    /**
     * 提示消息
     */
    String message() default "不允许重复提交,请稍候再试";
}

3.创建可重复读取流的包装类

public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
        super(request);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        //获取请求体
        body = HttpHelper.getBodyString(request).getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream basis = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() {
                return basis.read();
            }

            @Override
            public int available() {
                return body.length;
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

4.创建过滤器,重新包装为可重复读取的request流,方便后续拦截器能读取流中的body数据

public class RepeatableFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (request instanceof HttpServletRequest && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            //包装请求,构建新的可重复读的request流
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }
        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {

    }
}

5.创建拦截器,拦截注解标记的方法,验证是否重复提交

@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
    /**
     * 拦截注解方法
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            //获取方法注解
            RepeatSubmit methodAnnotation = method.getAnnotation(RepeatSubmit.class);
            //获取类注解
            RepeatSubmit classAnnotation = method.getDeclaringClass().getAnnotation(RepeatSubmit.class);
            //优先取方法注解参数
            RepeatSubmit repeatSubmit = !ObjectUtils.isEmpty(methodAnnotation) ? methodAnnotation : classAnnotation;
            //拦截
            if (repeatSubmit != null && (this.isRepeatSubmit(request, repeatSubmit))) {
                String message = repeatSubmit.message();
                try {
                    response.setStatus(200);
                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().print(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return false;
            }
            //放行
            return true;
        } else {
            return true;
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

6.创建拦截器子类,由子类实现验证重复提交规则

@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
    public static final String REPEAT_PARAMS = "repeatParams";

    public static final String REPEAT_TIME = "repeatTime";

    // 令牌自定义标识
    private static final String HEADER = "Authorization";

    @Resource
    private RedisCache redisCache;

    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {
        String nowParams = "";
        //获取可重复读取的request流,并从流中获取body
        if (request instanceof RepeatedlyRequestWrapper) {
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            //获取body
            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
        }
        // body参数为空,获取Parameter的数据
        if (StringUtils.isEmpty(nowParams)) {
            nowParams = JSON.toJSONString(request.getParameterMap());
        }
        Map nowDataMap = new HashMap<>();
        //设置请求参数
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        //设置请求时间
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 唯一值(没有消息头则使用请求地址)
        String submitKey = StringUtils.trimToEmpty(request.getHeader(HEADER));

        // 唯一标识(指定key + url + 消息头)
        String cacheRepeatKey = "repeat_submit:" + url + submitKey;

        //获取缓存对象
        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        Map sessionMap;
        if (sessionObj != null) {
            sessionMap = (Map) sessionObj;
            //比较请求参数以及请求时间,为true代表短时间内多次重复提交
            if (compareParams(nowDataMap, sessionMap) && compareTime(nowDataMap, sessionMap, annotation.interval())) {
                return true;
            }
        }
        //如果不是短时间内重复提交则添加或更新缓存对象
        redisCache.setCacheObject(cacheRepeatKey, nowDataMap, annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    /**
     * 判断参数是否相同
     */
    private boolean compareParams(Map nowMap, Map preMap) {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }

    /**
     * 判断两次间隔时间
     */
    private boolean compareTime(Map nowMap, Map preMap, int interval) {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        return (time1 - time2) < interval;
    }
}

7.创建过滤器配置类将过滤器注册到过滤器链中

public class FilterConfig {

    /**
     * 防止重复提交过滤器
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new RepeatableFilter());
        registration.addUrlPatterns("/*");
        registration.setName("repeatableFilter");
        //优先级为最低
        registration.setOrder(Ordered.LOWEST_PRECEDENCE);
        return registration;
    }

}

8.创建拦截器配置类将拦截器注册到容器当中

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;


    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
    }
}

9.创建reids配置类

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

    static class FastJson2JsonRedisSerializer implements RedisSerializer {
        public final Charset defaultCharset = StandardCharsets.UTF_8;

        private final Class clazz;

        public FastJson2JsonRedisSerializer(Class clazz) {
            super();
            this.clazz = clazz;
        }

        @Override
        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(defaultCharset);
        }

        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, defaultCharset);

            return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
        }
    }

} 
  

 10.创建一个测试Controller

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/paramTest")
    @RepeatSubmit
    public String paramTest(@RequestParam("id") String id) {
        return id;
    }

    @PostMapping("/bodyTest")
    @RepeatSubmit
    public User bodyTest(@RequestBody() User user) {
        return user;
    }

}

11.使用ApiPost进行测试

测试param传参

第一次请求:

SpingBoot接口防刷_第1张图片

5s内再次请求: 

SpingBoot接口防刷_第2张图片

测试body传参

第一次请求:

SpingBoot接口防刷_第3张图片

5s内再次请求: 

SpingBoot接口防刷_第4张图片

你可能感兴趣的:(redis,spring,缓存,springboot)