Spring Boot:如何在OncePerRequestFilter中仅修改认证场景响应体?
在Spring OncePerRequestFilter中修改响应体的实现方案
可以实现,核心是通过HttpServletResponseWrapper包装原始响应,缓存输出内容后再修改并写入实际响应。以下是具体实现步骤:
1. 自定义响应包装器
创建一个继承HttpServletResponseWrapper的类,用于缓存响应输出流的内容——因为原始响应的输出流只能写入一次,无法直接修改:
public class ModifiableResponseWrapper extends HttpServletResponseWrapper { private final ByteArrayOutputStream cachedContent; private ServletOutputStream outputStream; private PrintWriter writer; public ModifiableResponseWrapper(HttpServletResponse response) { super(response); this.cachedContent = new ByteArrayOutputStream(); } @Override public ServletOutputStream getOutputStream() throws IOException { if (writer != null) { throw new IllegalStateException("PrintWriter already opened"); } if (outputStream == null) { outputStream = new CachedServletOutputStream(cachedContent); } return outputStream; } @Override public PrintWriter getWriter() throws IOException { if (outputStream != null) { throw new IllegalStateException("ServletOutputStream already opened"); } if (writer == null) { writer = new PrintWriter(new OutputStreamWriter(cachedContent, getCharacterEncoding())); } return writer; } @Override public void flushBuffer() throws IOException { if (writer != null) { writer.flush(); } if (outputStream != null) { outputStream.flush(); } super.flushBuffer(); } public byte[] getCachedContent() { flushBuffer(); return cachedContent.toByteArray(); } private static class CachedServletOutputStream extends ServletOutputStream { private final ByteArrayOutputStream outputStream; public CachedServletOutputStream(ByteArrayOutputStream outputStream) { this.outputStream = outputStream; } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener listener) { // 同步场景下无需实现 } @Override public void write(int b) throws IOException { outputStream.write(b); } } }
2. 实现自定义过滤器
在OncePerRequestFilter中使用上述包装器,拦截并修改认证错误的响应体:
@Component @Order(SecurityProperties.BASIC_AUTH_ORDER + 1) // 确保在Spring Security认证过滤器之后执行 public class AuthResponseFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; // 注入Spring容器中的ObjectMapper实例 public AuthResponseFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ModifiableResponseWrapper wrappedResponse = new ModifiableResponseWrapper(response); // 让后续过滤器/控制器处理请求 filterChain.doFilter(request, wrappedResponse); // 匹配认证错误场景:401状态码且存在WWW-Authenticate头 if (response.getStatus() == HttpStatus.UNAUTHORIZED.value() && wrappedResponse.getHeader("WWW-Authenticate") != null) { if (!response.isCommitted()) { // 重置响应缓冲区,清除原始内容 response.resetBuffer(); // 构造自定义JSON响应体 String authErrorMsg = wrappedResponse.getHeader("WWW-Authenticate"); Map<String, Object> errorResp = new HashMap<>(); errorResp.put("code", HttpStatus.UNAUTHORIZED.value()); errorResp.put("message", "认证失败: " + authErrorMsg); // 设置响应头 response.setContentType("application/json;charset=UTF-8"); byte[] respBytes = objectMapper.writeValueAsBytes(errorResp); response.setContentLength(respBytes.length); // 写入修改后的响应体 response.getOutputStream().write(respBytes); response.flushBuffer(); } } else { // 无需修改时,将缓存的原始响应内容写入实际响应 byte[] cachedContent = wrappedResponse.getCachedContent(); if (cachedContent.length > 0 && !response.isCommitted()) { response.getOutputStream().write(cachedContent); response.flushBuffer(); } } } }
关键注意事项
- 过滤器顺序:必须保证该过滤器在Spring Security的认证过滤器之后执行,否则无法捕获到认证产生的响应。通过
@Order注解调整顺序,SecurityProperties.BASIC_AUTH_ORDER是Spring Security基础认证过滤器的默认顺序,加1确保在其后执行。 - 响应提交判断:修改前必须检查
response.isCommitted(),如果响应已提交,无法再修改内容,避免抛出异常。 - ObjectMapper注入:直接使用Spring容器中的
ObjectMapper实例,避免重复创建,同时保证配置统一。
内容的提问来源于stack exchange,提问作者Onki
相关产品推荐
相关产品推荐

