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
相关产品推荐
相关产品推荐

