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
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传参
第一次请求:
5s内再次请求:
测试body传参
第一次请求:
5s内再次请求: