赞
踩
一开始遇到的重复提交的bug,前端可以做到控制,但是后端做个控制就更好了。于是就有了这次的学习。
一、重复提交
想法是将request的uri和body做sha,存放在缓存中(内存,redis),做key。给每个session做一个唯一标识符,做value。判断是否重复提交,判断相同key的value是否一致就可以了。
其中 FormHttpMessageConverter.DEFAULT_CHARSET = Charset.forName("UTF-8");
- @Component
- public class PreventResubmissionInterceptor extends HandlerInterceptorAdapter {
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws Exception {
- // 该方法适用于新增,修改,删除操作
- if(RequestMethod.PUT.name().equals(request.getMethod())
- || RequestMethod.POST.name().equals(request.getMethod())
- || RequestMethod.DELETE.name().equals(request.getMethod())
- || RequestMethod.PATCH.name().equals(request.getMethod())){
- // 获取request的uri和body
- String uriBody = DigestUtils.sha256Hex(getRequestUriBody(request));
-
- // 将uriBody放入缓存中,并且判断value(sessionId)是否一致
- ...
- }
- return true;
- }
-
- @Override
- public void afterCompletion(
- HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
- throws Exception {
- // 请求结束,清除缓存
- ...
- }
-
- /**
- * 获取request body中的数据和URI
- * 组合成json
- * 最后转成string输出
- *
- * @param request
- * @return
- */
- private String getRequestUriBody(HttpServletRequest request){
- BufferedReader bufferedReader = null;
- InputStream inputStream = null;
- StringBuilder sb = new StringBuilder("");
- try {
-
- inputStream = request.getInputStream();
- bufferedReader = new BufferedReader(new InputStreamReader(
- inputStream, FormHttpMessageConverter.DEFAULT_CHARSET));
- String line = "";
- while ((line = bufferedReader.readLine()) != null) {
- sb.append(line);
- }
- }catch (IOException e){
- e.printStackTrace();
- }finally{
- if (null != inputStream){
- try{
- inputStream.close();
- }catch (IOException e){
- e.printStackTrace();
- }
- }
- if (null != bufferedReader){
- try{
- bufferedReader.close();
- }catch (IOException e){
- e.printStackTrace();
- }
- }
- }
- // 用JSON做格式化
- JSONObject jsonObject = new JSONObject();
- jsonObject.put("body", sb.toString());
- jsonObject.put("uri", request.getRequestURI());
- return jsonObject.toJSONString();
- }
-
- }
再加上注册的的部分就好了。。
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new SessionTraceInterceptor());
- // 注册防止重复提交的Interceptor
- // 这里最好加上url过滤,如果有做url控制
- registry.addInterceptor(new PreventResubmissionInterceptor(resubmissionCacheService))
- .addPathPatterns(...);
- super.addInterceptors(registry);
- }
二、可重复读取request
在其中涉及到读取requset.body,运行时会报错。因为其只能读一次,ServletInputStream没有reset/mark,不能做reset,因此读过一次之后,就不会重头开始读了。为了解决这个问题,给其加一个包装类,这样每次读取从缓存中拿就好。
包装类,将request.body读出来,缓存起来。之后读request.body都是读这里缓存起来的。
method:readBytes中在字符串前加一个空格。这是因为之前出了一个问题,在PreventResubmissionInterceptor .getRequestUriBody中去读body时,第一个元素读不到。再跟源码之后没有发现问题,并且问题不在出现了。所以这里保留,待后续遇到再跟进处理。
- public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
-
- private final byte[] body;
-
- public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException {
- super(request);
- body = readBytes(request.getInputStream(), FormHttpMessageConverter.DEFAULT_CHARSET.name());
- }
-
- @Override
- public ServletInputStream getInputStream() throws IOException {
- final ByteArrayInputStream bais = new ByteArrayInputStream(body!=null?body:new byte[0]);
- return new ServletInputStream() {
- @Override
- public boolean isFinished() {
- return false;
- }
-
- @Override
- public boolean isReady() {
- return false;
- }
-
- @Override
- public void setReadListener(ReadListener listener) {
-
- }
-
- @Override
- public int read() throws IOException {
- return bais.read();
- }
- };
- }
-
- /**
- * 通过BufferedReader和字符编码集转换成byte数组
- *
- * @param servletInputStream
- * @param encoding
- * @return
- * @throws IOException
- */
- private byte[] readBytes(ServletInputStream servletInputStream,String encoding) throws IOException{
- BufferedReader bufferedReader = null;
- String str = "",retStr="";
- try {
- bufferedReader = new BufferedReader(new InputStreamReader(
- servletInputStream, Charset.forName(encoding)));
- while ((str = bufferedReader.readLine()) != null) {
- retStr += str;
- }
- if (StringUtils.isNotBlank(retStr))
- return retStr.getBytes(Charset.forName(encoding));
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- }
- return null;
- }
- }
filter,实现request的包装类
- public class RepeatableReadFilter implements Filter {
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
-
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
- ServletRequest requestWrapper = null;
- if (request instanceof HttpServletRequest) {
- requestWrapper = new RepeatableReadRequestWrapper((HttpServletRequest) request);
- }
- if (null == requestWrapper) {
- chain.doFilter(request, response);
- } else {
- chain.doFilter(requestWrapper, response);
- }
- }
-
- @Override
- public void destroy() {
-
- }
- }
注册filter
- @Bean
- public FilterRegistrationBean repeatedlyReadFilter() {
- FilterRegistrationBean registration = new FilterRegistrationBean();
- RepeatableReadFilter repeatableReadFilter = new RepeatableReadFilter();
- registration.setFilter(repeatableReadFilter);
- // 这里最好加上url过滤,如果有做url控制
- registration.addUrlPatterns(...);
- return registration;
- }
如果缓存使用redis还需要考虑原子性问题,建议使用内存加redis。比如说A,B请求一样,A请求通过验证redis中没有该记录,还没有将记录写入redis中,此时B请求也通过验证。那么就还会出现重复提交的问题。这是需要做一个控制版本号的机制,将value的初始值为0,之后获取锁+1,如果>1,就获取不到并且过期时间也需要设置。
更多加锁方式:
详细:https://blog.csdn.net/Dennis_ukagaka/article/details/78072274
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。