1. 使用场景
在Web应用程序中,用户可能会重复提交表单,例如在点击提交按钮后仍连续多次点击、由于网络延迟造成用户误以为提交未成功而再次提交。这可能导致一些问题,例如重复的数据插入或重复的业务逻辑处理。接口防重处理的主要作用是在用户提交表单请求后,对请求进行拦截和处理,防止重复提交
2. 解决方案
生成唯一的key,key的组成为url+token+参数,意味着每一个用户的每一个请求都对应redis中的一个key,该key对应的值存空字符串即可(因为我们只需要key来保证唯一性,值不存东西也行)。首先,我们获取这个key,如果redis中没有这个key说明用户第一次请求,则不拦截请求并将key存进redis。如果redis中有key,说明在限制时间里(key没过期)用户再次进行了请求,则拦截返回错误信息
以上逻辑可以使用AOP或者拦截器、过滤器实现
3. 实现
3.1. 解决HttpServletRequest 流数据不可重复读
在项目中经常出现多次读取HTTP请求体的情况,这时候可能就会报错,原因是读取HTTP请求体的操作,最终都要调用HttpServletRequest的getInputStream()方法和getReader()方法,而这两个方法总共只能被调用一次,第二次调用就会报错。I/O error while reading input message; nested exception is java.io.IOException: Stream closed
实现一个过滤器
包装请求之后,缓存的值总是存在,所以可以多次读取请求体
@Component
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
MultipleReadHttpRequest wrappedRequest = new MultipleReadHttpRequest(currentRequest);
chain.doFilter(wrappedRequest, servletResponse);
}
}
自定义一个请求包装类
public class MultipleReadHttpRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedContent;
public MultipleReadHttpRequest(HttpServletRequest request) throws IOException {
super(request);
// Read the request body and populate the cachedContent
cachedContent = new ByteArrayOutputStream();
InputStream inputStream = request.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
cachedContent.write(buffer, 0, bytesRead);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedContent.toByteArray());
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 byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(cachedContent.toByteArray()));
return new BufferedReader(reader);
}
}
注册拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private DuplicateRequestInterceptor duplicateRequestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(duplicateRequestInterceptor).addPathPatterns("/**");
}
}
实现拦截器
@Component
@Slf4j
public class DuplicateRequestInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String REDIS_KEY_PREFIX = "duplicate_request_";
private static final long EXPIRE_TIME = 10; // 过期时间,单位为秒
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 替换 HttpServletRequest 为 MultipleReadHttpRequest
MultipleReadHttpRequest wrapper = new MultipleReadHttpRequest(request);
String url = wrapper.getRequestURI();
// 从请求头中获取token
String token = Optional.ofNullable(wrapper.getHeader("Satoken")).orElse("");
// 获取请求参数
StringBuilder params = new StringBuilder();
params.append(IOUtils.toString(wrapper.getInputStream(), "UTF-8"));
String key = REDIS_KEY_PREFIX + url + "_" + token + "_" + params.toString();
Boolean exist = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exist)) {
log.info(key.toString());
try (PrintWriter writer = response.getWriter()) {
writer.write("DuplicateRequest");
}
return false; // 拦截请求
} else {
// 将key存入Redis,设置过期时间
redisTemplate.opsForValue().set(key, "", EXPIRE_TIME, TimeUnit.SECONDS);
return true; // 放行请求
}
}
}