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

Keycloak策略执行器权限评估后请求体丢失返回400如何解决?

问题根因

Servlet规范中HTTP请求的InputStream默认只能被读取一次。Keycloak策略执行器在权限校验阶段为了提取request.body中的字段,已经提前读取了请求体的输入流,后续Spring MVC处理请求时,@RequestBody注解对应的转换器无法再次读取到流内容,就会抛出「请求体缺失」的400错误。


解决方案:配置可重复读取的请求体包装器

你需要新增一个过滤器,在Keycloak的策略执行器执行前,把原始请求包装成支持重复读取输入流的请求对象,保证Keycloak读取后,Spring MVC还能正常获取请求体。

1. 实现请求体缓存包装类

import org.springframework.util.StreamUtils;
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;

public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBody;

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 读取原始请求体并缓存到字节数组
        this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 每次调用都返回基于缓存字节数组的新输入流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(javax.servlet.ReadListener readListener) {
                // 无需额外实现
            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }
}

2. 实现过滤器,包装请求对象

设置过滤器的执行顺序早于Keycloak的策略执行器过滤器即可:

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // 优先级高于Keycloak默认过滤器
public class CacheRequestBodyFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 仅针对需要读取请求体做权限校验的接口开启,避免不必要的性能损耗
        if (request instanceof HttpServletRequest
                && "POST".equalsIgnoreCase(((HttpServletRequest) request).getMethod())
                && ((HttpServletRequest) request).getRequestURI().startsWith("/api/example")) {
            CachedBodyHttpServletRequestWrapper wrappedRequest = new CachedBodyHttpServletRequestWrapper((HttpServletRequest) request);
            chain.doFilter(wrappedRequest, response);
            return;
        }
        chain.doFilter(request, response);
    }
}

注意事项

如果你的业务接口请求体普遍较大,建议不要全局启用该过滤器,只针对需要基于请求体做权限校验的路径开启,避免额外的内存开销。

内容的提问来源于stack exchange,提问作者kbodurri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 11:45:05