如何避免在过滤器后在 dispatcherServlet 中关闭输入流
Posted
技术标签:
【中文标题】如何避免在过滤器后在 dispatcherServlet 中关闭输入流【英文标题】:How avoid input stream closed in dispatcherServlet after a filter 【发布时间】:2018-11-28 14:56:15 【问题描述】:我需要将有关为应用程序发送的请求/响应的所有信息保存为 http 状态、当前时间、令牌、请求 URI 等。它是一个 API,资源是:
POST localhost:8080/v1/auth/login 在请求身份验证时使用电子邮件和密码。响应是一个 JWT 令牌。
GET localhost:8080/v1/auth/rules 在请求的标头中带有令牌。响应是一个正文,其中包含有关令牌所有者的信息,例如电子邮件和姓名。
为此,我的方法重写了 doDispatch 方法:
LogDispatcherServlet
@Component
public class LogDispatcherServlet extends DispatcherServlet
@Autowired
private LogRepository logRepository;
private static final Logger logger = LoggerFactory.getLogger(LogDispatcherServlet.class);
@Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception
if (!(request instanceof ContentCachingRequestWrapper))
request = new ContentCachingRequestWrapper(request);
if (!(response instanceof ContentCachingResponseWrapper))
response = new ContentCachingResponseWrapper(response);
HandlerExecutionChain handler = getHandler(request);
try
super.doDispatch(request, response);
finally
try
ApiLog log = ApiLog.build(request, response, handler, null);
logRepository.save(log);
updateResponse(response);
catch (UncheckedIOException e)
logger.error("UncheckedIOException", e);
catch (Exception e)
logger.error("an error in auth", e);
private void updateResponse(HttpServletResponse response) throws IOException
ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
responseWrapper.copyBodyToResponse();
ApiLog.build 负责获取有关请求的示例信息,LogDispatcherServlet 对 localhost:8080/v1/auth/rules 中的 GET 工作正常 em>。
ApiLog
public static ApiLog build(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain handler, Authentication auth)
ApiLog log = new ApiLog();
log.setHttpStatus(response.getStatus());
log.setHttpMethod(request.getMethod());
log.setPath(request.getRequestURI());
log.setClientIp(request.getRemoteAddr());
try
if (request.getReader() != null)
log.setBodyRequest(getRequestPayload(request));
catch (IOException e)
e.printStackTrace();
if (handler != null)
log.setJavaMethod(handler.toString());
if (request.getHeader("Authorization") != null)
log.setToken(request.getHeader("Authorization"));
else if (response.getHeader("Authorization") != null)
log.setToken(response.getHeader("Authorization"));
log.setResponse(getResponsePayload(response));
log.setCreated(Instant.now());
logger.debug(log.toString());
return log;
@NotNull
private static String getRequestPayload(HttpServletRequest request)
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
try
return wrapper
.getReader()
.lines()
.collect(Collectors.joining(System.lineSeparator()));
catch (IOException e)
e.printStackTrace();
return "";
@NotNull
private static String getResponsePayload(HttpServletResponse responseToCache)
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(responseToCache, ContentCachingResponseWrapper.class);
if (wrapper != null)
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0)
int length = Math.min(buf.length, 5120);
try
return new String(buf, 0, length, wrapper.getCharacterEncoding());
catch (UnsupportedEncodingException ex)
logger.error("An error occurred when tried to logging request/response");
return "";
我最大的问题是:我正在使用 Spring Security 生成 JWT 令牌,因此发送到 /v1/auth/login 的所有请求都被重定向到过滤器。
应用安全
@Configuration
@EnableWebSecurity
public class AppSecurity extends WebSecurityConfigurerAdapter
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
httpSecurity.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.and()
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class);
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(customUserDetailsService);
认证成功后,过滤器必须调用 LogDispatcherServlet 来持久化请求和响应。 /login 没有 Controller,只有 JWTLoginFilter。
JWTLoginFilter
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter
@Autowired
JWTLoginFilter(String url, AuthenticationManager authManager)
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException
AccountCredentials credentials = new ObjectMapper()
.readValue(request.getInputStream(), AccountCredentials.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
Md5.getHash(credentials.getPassword()),
Collections.emptyList()
)
);
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain,
Authentication auth) throws IOException, ServletException
TokenAuthenticationService.addAuthentication(response, auth.getName());
//Must call LogDispatcherServlet
filterChain.doFilter(request, response);
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
super.unsuccessfulAuthentication(request, response, failed);
//Must call LogDispatcherServlet
但它不适用于 /login。当 ApiLog 尝试在 getRequestPayload 中获取请求正文时,我得到一个 java.io.IOException: Stream closed
我能做些什么来避免这种情况? JWTLoginFilter 需要知道身份验证的请求正文和 LogDispatcherServlet,但 request.getInputStream() 是在 attemptAuthentication 中调用的。是否有另一种不那么复杂的解决方案?
【问题讨论】:
【参考方案1】:我认为您不需要更新 Spring 的 DispatcherServlet
。我将创建一个过滤器(在链中的第一个位置),将原始请求/响应包装在允许缓存的对象中(例如ContentCachingRequestWrapper
/ ContentCachingResponseWrapper
)。
在您的过滤器中,您只需要执行以下操作:
doFilter(chain, req, res)
ServletRequest wrappedRequest = ...
ServletResponse wrappedResponse = ...
chain.doFilter(wrappedRequest, wrappedResponse);
你可以注册一个HandlerInterceptor
public class YourHandlerIntercepter extends HandlerInterceptorAdapter
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
if (handler instanceof HandlerMethod)
HandlerMethod handlerMethod = (HandlerMethod)
ApiLog log = ApiLog.build(request, response, handler, null);
logRepository.save(log);
return true;
您最终需要更改您的 ApiLog 方法,使其使用 MethodHandler
而不是 HandlerExecutionChain
【讨论】:
以上是关于如何避免在过滤器后在 dispatcherServlet 中关闭输入流的主要内容,如果未能解决你的问题,请参考以下文章
java.lang.IllegalStateException:未找到 WebApplicationContext:未注册 ContextLoaderListener 或 DispatcherServ