代码地址
ruoyi脚手架是通过参数去判断:
- 把请求缓存起来到redis里面去【请求地址、请求参数…】
- 比如:请求地址和请求参数都是一样的话,10秒只能就拒绝重复提交
但这个方法存在一个问题:如果请求的参数是json,一旦我提取出来,将来在接口里面就提取不到了,所以我们先要把这个问题解决掉
**所需依赖:**springweb、redis、aop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <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> </dependencies>
|
请求参数格式问题
io流(json)可能产生的问题复现
io/github/lcdzzz/repeat_submit/interceptor/RepeatSubmitInterceptor.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@Component public class RepeatSubmitInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("request.getReader().readLine() = " + request.getReader().readLine()); return true; }
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
|
io/github/lcdzzz/repeat_submit/config/webConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import io.github.lcdzzz.repeat_submit.interceptor.RepeatSubmitInterceptor; 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;
@Configuration public class webConfig implements WebMvcConfigurer { @Autowired RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); } }
|
io/github/lcdzzz/repeat_submit/controller/HelloController.java
1 2 3 4 5 6 7 8 9 10 11 12
| import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;
@RestController public class HelloController { @PostMapping("/hello") public String hello(@RequestBody String json){ return json; }
}
|
**【注】**访问/hello时首先会被拦截器拦截,拦截器里面的request.getReader().readLine()
与接口中的@RequestBody
都是通过io流实现的,然而io流使用过一次以后数据就不存在了。所以会出现报错。
问题解决
**解决思路:**用装饰者模式将HttpServletRequest重新处理一下
RepeatableReadRequestWrapper 类
这个类是一个继承自 HttpServletRequestWrapper
的包装类。主要作用是:
- 在构造函数中,读取请求的输入流,并将其保存在
bytes
数组中。
- 重写
getReader()
和 getInputStream()
方法,返回一个新的 BufferedReader
和 ServletInputStream
实例,这两者都是用 bytes
数组来构造的。
实际上,这个类的目的是允许请求的输入流(请求体)能够被多次读取,因为默认情况下,Servlet的输入流只能读取一次。
io/github/lcdzzz/repeat_submit/request/RepeatableReadRequestWrapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.*;
public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper { private final byte[] bytes; public RepeatableReadRequestWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException { super(request); request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); bytes=request.getReader().readLine().getBytes(); }
@Override public BufferedReader getReader() throws IOException { return new BufferedReader((new InputStreamReader(getInputStream()))); }
@Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream(bytes); return new ServletInputStream() { @Override public boolean isFinished() { return false; }
@Override public boolean isReady() { return false; }
@Override public void setReadListener(ReadListener readListener) {
}
@Override public int read() throws IOException { return bais.read(); } @Override public int available() throws IOException { return bytes.length; } };
} }
|
RepeatableRequestFilter 类
这个类是一个实现了 Filter
接口的过滤器。主要作用是:
- 检查请求的
Content-Type
是否以 “application/json” 开头,如果是,就创建一个 RepeatableReadRequestWrapper
对象,并替换原始的 HttpServletRequest
,以允许请求体的多次读取。
- 继续执行过滤器链,将请求和响应传递给下一个过滤器或目标 Servlet。
这个过滤器的作用是在请求为 JSON 格式时,将原始的请求对象替换为具有多次读取能力的包装类,从而防止在处理请求时由于读取请求体而导致的问题。
io/github/lcdzzz/repeat_submit/filter/RepeatableRequestFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import io.github.lcdzzz.repeat_submit.request.RepeatableReadRequestWrapper; import org.springframework.util.StringUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
public class RepeatableRequestFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; if (StringUtils.startsWithIgnoreCase(request.getContentType(),"application/json")){ RepeatableReadRequestWrapper requestWrapper = new RepeatableReadRequestWrapper(request, (HttpServletResponse) servletResponse); filterChain.doFilter(requestWrapper,servletResponse); return; } filterChain.doFilter(servletRequest,servletResponse); } }
|
webConfig 类
这个类是一个Spring Boot的配置类,实现了 WebMvcConfigurer
接口。主要作用是:
- 注册一个自定义的拦截器
RepeatSubmitInterceptor
,该拦截器用于防止表单重复提交。
- 注册一个自定义的过滤器
RepeatableRequestFilter
,该过滤器用于处理重复请求。这里通过 FilterRegistrationBean
配置了过滤器的注册。
io/github/lcdzzz/repeat_submit/config/webConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import io.github.lcdzzz.repeat_submit.filter.RepeatableRequestFilter; import io.github.lcdzzz.repeat_submit.interceptor.RepeatSubmitInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class webConfig implements WebMvcConfigurer { @Autowired RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); }
@Bean FilterRegistrationBean<RepeatableRequestFilter> repeatableRequestFilterFilterRegistrationBean(){ FilterRegistrationBean<RepeatableRequestFilter> bean = new FilterRegistrationBean<>(); bean.setFilter(new RepeatableRequestFilter()); bean.addUrlPatterns("/*"); return bean; } }
|
综合来看,这三段代码一起实现了一个防止重复提交的机制,在请求为 JSON 格式时,通过替换请求对象的方式,允许请求体的多次读取。同时,通过拦截器和过滤器的配置,使这个机制能够在整个应用中生效。
过滤器先执行,拦截器后执行。因为拦截器是servlet调用的,过滤器比servlet先执行。所以顺序应该是在过滤器中先把请求的格式切换过来。
对应ruoyi/tienchin的实现
tienchin-common/src/main/java/com/lcdzzz/common/filter/RepeatedlyRequestWrapper.java
===io/github/lcdzzz/repeat_submit/request/RepeatableReadRequestWrapper.java
tienchin-common/src/main/java/com/lcdzzz/common/filter/RepeatableFilter.java
===io/github/lcdzzz/repeat_submit/filter/RepeatableRequestFilter.java
tienchin-framework/src/main/java/com/lcdzzz/framework/config/FilterConfig.java
===io/github/lcdzzz/repeat_submit/config/webConfig.java
防止重复提交
思路:把【你是谁、请求地址、请求参数…】存到redis里面去。
定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatSubmit {
int interval() default 5000;
String message() default "不允许重复提交,请稍后再试"; }
|
redis工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component public class RedisCahe { @Autowired RedisTemplate redisTemplate;
public <T> void setRedisTemplate(final String key, final T value, Integer timeout, final TimeUnit timeUnit){
redisTemplate.opsForValue().set(key,value,timeout,timeUnit); }
public <T> T getCacheObject(final String key){ ValueOperations<String,T> valueOperations = redisTemplate.opsForValue(); return valueOperations.get(key); } }
|
配置注解
之前配置注解都是用aop的,这次试一试在拦截器地方配置
HandlerMethod: 会把我每定义的一个接口方法封装成一个对象。就是接口方法的各种信息【说属于哪个类的,方法泛型是什么,方法地址,方法参数,返回值…】又被分装成一个新的类,就叫HandlerMethod。
如果所有的像这样的接口方法,最终都会被封装成HandlerMethod。
1 2 3 4
| @PostMapping("/hello") public String hello(@RequestBody String json){ return json; }
|
RepeatSubmitInterceptor
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| @Component public class RepeatSubmitInterceptor implements HandlerInterceptor { public static final String REPEAT_PARAMS = "repeat_params"; public static final String REPEAT_TIME = "repeat_time"; public static final String REPEAT_SUBMIT_KEY = "repeat_submit_key"; public static final String HEADER = "Authorization"; @Autowired RedisCahe redisCahe; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class); if (repeatSubmit != null) { if (isRepeatSubmit(request, repeatSubmit)) { Map<String, Object> map = new HashMap<>(); map.put("status", 500); map.put("message", repeatSubmit.message()); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(map)); return false; } } } return true; }
private boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit repeatSubmit) { String nowParams = ""; if (request instanceof RepeatableReadRequestWrapper) { try { nowParams = ((RepeatableReadRequestWrapper) request).getReader().readLine(); } catch (IOException e) { e.printStackTrace(); } } if (StringUtils.isEmpty(nowParams)) { try { nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap()); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } Map<String, Object> nowDataMap = new HashMap<>(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); String requestURI = request.getRequestURI(); String header = request.getHeader(HEADER); String cacheKey = REPEAT_SUBMIT_KEY + requestURI + header.replace("Bearer ", ""); Object cacheObject = redisCahe.getCacheObject(cacheKey); if (cacheObject != null) { Map<String, Object> map = (Map<String, Object>) cacheObject; if (compareParams(map, nowDataMap) && compareTime(map, nowDataMap, repeatSubmit.interval())) { return true; } } redisCahe.setRedisTemplate(cacheKey, nowDataMap, repeatSubmit.interval(), TimeUnit.MILLISECONDS); return false; }
private boolean compareTime(Map<String, Object> map, Map<String, Object> nowDataMap, int interval) { Long time1 = (Long) map.get(REPEAT_TIME); Long time2 = (Long) nowDataMap.get(REPEAT_TIME); if ((time2 - time1) < interval) { return true; } return false; }
private boolean compareParams(Map<String, Object> map, Map<String, Object> nowDataMap) { String nowParams = (String) nowDataMap.get(REPEAT_PARAMS); String dataParams = (String) map.get(REPEAT_PARAMS); return nowParams.equals(dataParams);
}
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
|
解释:
这段代码是一个拦截器(Interceptor)中的一个方法
具体来说,这个方法用于在处理请求之前检查是否存在重复提交。它接收三个参数:
HttpServletRequest request
:表示 HTTP 请求的对象,包含了客户端发送的请求信息。
HttpServletResponse response
:表示 HTTP 响应的对象,用于向客户端发送响应信息。
Object handler
:表示处理请求的处理器对象。
该方法首先判断handler
是否是HandlerMethod
类型的实例,这是因为处理请求的方法通常是通过HandlerMethod
来表示的。如果是,则获取对应的方法对象Method
。
接着,通过method.getAnnotation(RepeatSubmit.class)
获取该方法上是否标注了RepeatSubmit
注解。如果存在该注解,说明该方法需要进行重复提交的校验。
然后,调用isRepeatSubmit(request, repeatSubmit)
方法来判断当前请求是否是重复提交。如果是重复提交,则返回一个错误响应,其中包括状态码和消息,并设置响应内容类型为 JSON 格式。
最后,如果不需要拦截该请求(即不是重复提交),则返回true
,表示允许继续执行后续的请求处理;如果需要拦截该请求(即是重复提交),则返回false
,表示不允许继续执行后续的请求处理。
测试
在/hello接口上添加@RepeatSubmit(interval = 10000)
,表示两个请求之间的间隔时间是10秒
对标ruoyi
拦截器:com/lcdzzz/framework/interceptor
【问题】若是两个不一样的人来请求一个接口,同样有可能被拦截下来,但这其实是不合理的
但是在ruoyi这个项目里面是没关系的,因为它肯定有Header。