You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 13:55:28