接口防重处理

Posted by Wh0ami-hy on February 27, 2024

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; // 放行请求  
        }  
    }  
}

本站总访问量