解决HttpServletRequest流数据不可重复读
背景介绍
甲方客户的生产系统,有安全风险预警和安全事件快速溯源要求,需要做一套日志管理规范。
要求我们接入的系统,要对用户登录、注册、密码修改等重要场景,严格按照提供的格式,输出相应的日志。
后续通过filebeat对接,收集我们系统上的日志信息。
简单来说,就是应用系统,处理接口请求时,统一打印相应日志。问题描述
成熟且常见的日志统一打印方案,就是使用AOP技术,自定义注解,在切面上使用环绕通知@Around,拦截请求,获取Controller类上方法的入参、出参即可。
奈何业务场景使用到的接口,以前的人在实现的时候,使用了如下方式:@RequestMapping(value = "/auth", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS }) public void auth(HttpServletRequest req, HttpServletResponse resp) { authService.auth(req, resp); }
把传参直接丢在 HttpServletRequest 中。
返回参数,又是采用 HttpServletResponse 输出。public void printResult(HttpServletRequest req, HttpServletResponse resp, String action, int code, String msg, Object result) { PrintWriter p = null; Ret ret = new Ret(); Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss") .serializeNulls() .create(); try { p = resp.getWriter(); ret.setRspCode(code); ret.setRspDesc(msg); ret.setData(result); p.write(gson.toJson(ret)); return; } catch (Exception e) { logger.error(e.getMessage()); } finally { p.flush(); p.close(); } }
不像平时熟练的做法,把具体入参和出参,用对象封装,直接放在方法上即可。
因为上面的做法,导致我们在拦截器中,想提前拦截请求获取传参,使用 request.getParameter() 等方法时,能拿到参数。 但是在具体接口业务流程中,再使用request.getParameter() 等方法,传入参数就获取不到了。
因为流只能被读一次。
因此就抛出一个问题:Request 和 Response 怎么重复读取?解决方案
使用request.getParameter() 等方法,最终会调用getInputStream方法。
需要重写HttpServletRequestWrapper包装类,在调用getInputStream方法时,将流数据同时写到缓存。
后面想获取参数,直接读取缓存数据即可。
这样就可以实现Request的内容多次读取。实现代码封装request
自定义类 ContentCachingRequestWrapperimport javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * * 重写 HttpServletRequestWrapper * * @Author: linzengrui * @Date: 2021/11/22 15:33 */ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public ContentCachingRequestWrapper(HttpServletRequest request) { super(request); StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))){ String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } body = sb.toString().getBytes(StandardCharsets.UTF_8); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); 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 inputStream.read(); } }; } public byte[] getBody() { return body; } }封装response
自定义类 ContentCachingResponseWrapper import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.*; /** * * 重写 HttpServletResponseWrapper * * @Author: linzengrui * @Date: 2021/11/22 19:45 */ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream byteArrayOutputStream; private ServletOutputStream servletOutputStream; private PrintWriter printWriter; public ContentCachingResponseWrapper(HttpServletResponse response) { super(response); byteArrayOutputStream = new ByteArrayOutputStream(); servletOutputStream = new ServletOutputStream() { @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { } @Override public void write(int b) throws IOException { byteArrayOutputStream.write(b); } }; printWriter = new PrintWriter(byteArrayOutputStream); } @Override public PrintWriter getWriter() { return printWriter; } @Override public ServletOutputStream getOutputStream() throws IOException { return servletOutputStream; } public byte[] toByteArray() { return byteArrayOutputStream.toByteArray(); } }过滤器 Filter 拦截请求
拦截器 LogFilter 使用上面封装的包装类,即可获取传参。@Slf4j @WebFilter(urlPatterns = "/*") public class LogFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 使用 重写 HttpServletRequestWrapper 的自定义包装类 ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest)request); // 使用 重写 HttpServletResponseWrapper 的自定义包装类 ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response); // 只能执行一次获取流方法 try(ServletOutputStream outputStream = response.getOutputStream()){ // 获取传参 String requestParamJson = new String(requestWrapper.getBody()); log.info("requestParamJson --> {}", requestParamJson); // 具体方法执行流程 chain.doFilter(requestWrapper, responseWrapper); // 触发获取流操作后,可以从缓存多次拿数据 String respDataJson = new String(responseWrapper.toByteArray()); log.info("respDataJson <-- {}", respDataJson); // TODO 写日志 // 需要重新写入内容,否则流无输出内容 outputStream.write(respDataJson.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); }catch (Exception e){ e.printStackTrace(); } } @Override public void destroy() { } }springboot 启动类添加注解 @ServletComponentScan
注意:启动类要加上注解@ServletComponentScan识别上面注入的Filter。import org.springframework.boot.SpringApplication; import org.springframework.boot.web.servlet.ServletComponentScan; @ServletComponentScan @SpringBootApplication public class SpringBootApplication { public static void main(String[] args) { SpringApplication.run(SpringBootApplication.class, args); } }
具体业务接口,原来的逻辑保持不变,仍然可以获取到入参。