当前位置:   article > 正文

重复提交&&可重复读取request&&redis锁_request 与 redis

request 与 redis

一开始遇到的重复提交的bug,前端可以做到控制,但是后端做个控制就更好了。于是就有了这次的学习。

一、重复提交

想法是将request的uri和body做sha,存放在缓存中(内存,redis),做key。给每个session做一个唯一标识符,做value。判断是否重复提交,判断相同key的value是否一致就可以了。

其中 FormHttpMessageConverter.DEFAULT_CHARSET = Charset.forName("UTF-8");

  1. @Component
  2. public class PreventResubmissionInterceptor extends HandlerInterceptorAdapter {
  3. @Override
  4. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
  5. throws Exception {
  6. // 该方法适用于新增,修改,删除操作
  7. if(RequestMethod.PUT.name().equals(request.getMethod())
  8. || RequestMethod.POST.name().equals(request.getMethod())
  9. || RequestMethod.DELETE.name().equals(request.getMethod())
  10. || RequestMethod.PATCH.name().equals(request.getMethod())){
  11. // 获取request的uri和body
  12. String uriBody = DigestUtils.sha256Hex(getRequestUriBody(request));
  13. // 将uriBody放入缓存中,并且判断value(sessionId)是否一致
  14. ...
  15. }
  16. return true;
  17. }
  18. @Override
  19. public void afterCompletion(
  20. HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
  21. throws Exception {
  22. // 请求结束,清除缓存
  23. ...
  24. }
  25. /**
  26. * 获取request body中的数据和URI
  27. * 组合成json
  28. * 最后转成string输出
  29. *
  30. * @param request
  31. * @return
  32. */
  33. private String getRequestUriBody(HttpServletRequest request){
  34. BufferedReader bufferedReader = null;
  35. InputStream inputStream = null;
  36. StringBuilder sb = new StringBuilder("");
  37. try {
  38. inputStream = request.getInputStream();
  39. bufferedReader = new BufferedReader(new InputStreamReader(
  40. inputStream, FormHttpMessageConverter.DEFAULT_CHARSET));
  41. String line = "";
  42. while ((line = bufferedReader.readLine()) != null) {
  43. sb.append(line);
  44. }
  45. }catch (IOException e){
  46. e.printStackTrace();
  47. }finally{
  48. if (null != inputStream){
  49. try{
  50. inputStream.close();
  51. }catch (IOException e){
  52. e.printStackTrace();
  53. }
  54. }
  55. if (null != bufferedReader){
  56. try{
  57. bufferedReader.close();
  58. }catch (IOException e){
  59. e.printStackTrace();
  60. }
  61. }
  62. }
  63. // 用JSON做格式化
  64. JSONObject jsonObject = new JSONObject();
  65. jsonObject.put("body", sb.toString());
  66. jsonObject.put("uri", request.getRequestURI());
  67. return jsonObject.toJSONString();
  68. }
  69. }

再加上注册的的部分就好了。。

  1. @Override
  2. public void addInterceptors(InterceptorRegistry registry) {
  3. registry.addInterceptor(new SessionTraceInterceptor());
  4. // 注册防止重复提交的Interceptor
  5. // 这里最好加上url过滤,如果有做url控制
  6. registry.addInterceptor(new PreventResubmissionInterceptor(resubmissionCacheService))
  7. .addPathPatterns(...);
  8. super.addInterceptors(registry);
  9. }

二、可重复读取request

在其中涉及到读取requset.body,运行时会报错。因为其只能读一次,ServletInputStream没有reset/mark,不能做reset,因此读过一次之后,就不会重头开始读了。为了解决这个问题,给其加一个包装类,这样每次读取从缓存中拿就好。

包装类,将request.body读出来,缓存起来。之后读request.body都是读这里缓存起来的。

method:readBytes中在字符串前加一个空格。这是因为之前出了一个问题,在PreventResubmissionInterceptor .getRequestUriBody中去读body时,第一个元素读不到。再跟源码之后没有发现问题,并且问题不在出现了。所以这里保留,待后续遇到再跟进处理。

  1. public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
  2. private final byte[] body;
  3. public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException {
  4. super(request);
  5. body = readBytes(request.getInputStream(), FormHttpMessageConverter.DEFAULT_CHARSET.name());
  6. }
  7. @Override
  8. public ServletInputStream getInputStream() throws IOException {
  9. final ByteArrayInputStream bais = new ByteArrayInputStream(body!=null?body:new byte[0]);
  10. return new ServletInputStream() {
  11. @Override
  12. public boolean isFinished() {
  13. return false;
  14. }
  15. @Override
  16. public boolean isReady() {
  17. return false;
  18. }
  19. @Override
  20. public void setReadListener(ReadListener listener) {
  21. }
  22. @Override
  23. public int read() throws IOException {
  24. return bais.read();
  25. }
  26. };
  27. }
  28. /**
  29. * 通过BufferedReader和字符编码集转换成byte数组
  30. *
  31. * @param servletInputStream
  32. * @param encoding
  33. * @return
  34. * @throws IOException
  35. */
  36. private byte[] readBytes(ServletInputStream servletInputStream,String encoding) throws IOException{
  37. BufferedReader bufferedReader = null;
  38. String str = "",retStr="";
  39. try {
  40. bufferedReader = new BufferedReader(new InputStreamReader(
  41. servletInputStream, Charset.forName(encoding)));
  42. while ((str = bufferedReader.readLine()) != null) {
  43. retStr += str;
  44. }
  45. if (StringUtils.isNotBlank(retStr))
  46. return retStr.getBytes(Charset.forName(encoding));
  47. } catch (IOException e) {
  48. e.printStackTrace();
  49. }finally {
  50. }
  51. return null;
  52. }
  53. }

filter,实现request的包装类

  1. public class RepeatableReadFilter implements Filter {
  2. @Override
  3. public void init(FilterConfig filterConfig) throws ServletException {
  4. }
  5. @Override
  6. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  7. ServletRequest requestWrapper = null;
  8. if (request instanceof HttpServletRequest) {
  9. requestWrapper = new RepeatableReadRequestWrapper((HttpServletRequest) request);
  10. }
  11. if (null == requestWrapper) {
  12. chain.doFilter(request, response);
  13. } else {
  14. chain.doFilter(requestWrapper, response);
  15. }
  16. }
  17. @Override
  18. public void destroy() {
  19. }
  20. }

 注册filter

  1. @Bean
  2. public FilterRegistrationBean repeatedlyReadFilter() {
  3. FilterRegistrationBean registration = new FilterRegistrationBean();
  4. RepeatableReadFilter repeatableReadFilter = new RepeatableReadFilter();
  5. registration.setFilter(repeatableReadFilter);
  6. // 这里最好加上url过滤,如果有做url控制
  7. registration.addUrlPatterns(...);
  8. return registration;
  9. }

 

如果缓存使用redis还需要考虑原子性问题,建议使用内存加redis。比如说A,B请求一样,A请求通过验证redis中没有该记录,还没有将记录写入redis中,此时B请求也通过验证。那么就还会出现重复提交的问题。这是需要做一个控制版本号的机制,将value的初始值为0,之后获取锁+1,如果>1,就获取不到并且过期时间也需要设置。

更多加锁方式:

详细:https://blog.csdn.net/Dennis_ukagaka/article/details/78072274

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/550795
推荐阅读
相关标签
  

闽ICP备14008679号