Spring 安全:反序列化请求 body 两次(oauth2 处理)
Spring Security: deserialize request body twice (oauth2 processing)
这个问题是我使用 Spring Security Oauth2 库进行的一些工作的结果。我已经设置了一个 oauth2 授权服务器和一个 oauth2 资源服务器,后者旨在根据访问令牌进行授权。
问题是通常访问令牌是在 header 中传递的,但是我们为其设置的大客户想要在 JSON 请求中传递访问令牌 body.您可以使用一个界面来设置自定义访问令牌提取,但它看起来像这样:
public interface TokenExtractor {
/**
* Extract a token value from an incoming request without authentication.
*
* @param request the current ServletRequest
* @return an authentication token whose principal is an access token (or null if there is none)
*/
Authentication extract(HttpServletRequest request);
}
因此,据我所知,我只能访问原始 HTTPServletRequest,我需要从中反序列化请求并提取访问令牌。
然而,更复杂的是请求 body 还包含处理所需的其他参数,因此我想将其反序列化为传递到我的控制器中的 DTO class ,像这样:
@RequestMapping("/oauth/someresource")
@Transactional
public Map<String, String> resource(@AuthenticationPrincipal UserDetails userDetails,
@RequestBody ClientRequestDto clientRequestDto) {
// Do some processing based on the request dto
}
我尝试在令牌提取器中手动反序列化请求,但随后出现错误 "java.lang.IllegalStateException: getReader() has already been called for this request"。
我正在集思广益,想出一些我可以研究的可能解决方案,到目前为止我已经想出了:
- 想办法重置输入流
- 反序列化令牌提取器中的 object,将其附加到原始请求 object,然后在我的控制器中访问原始请求 object 而不是使用 @RequestBody
- 与 2 类似,但找到一种方法来添加自定义反序列化器,该反序列化器获取附加到原始请求的 object 而不是处理请求的输入流。
无论如何,这些只是一些想法,如果有人对解决此问题的优雅方法有任何想法,我将不胜感激。
编辑:我确实发现了这个类似的问题:,最后一个答案确实有一个可能的解决方案(创建一个允许多个输入流读取的装饰器请求 class 并创建一个在包装 HttpServletRequest 的过滤器链的早期过滤)。这似乎可行,但有点繁重,所以我会留下这个看看是否有人还有其他想法。
所以我最终找到了另一个问题来解决我在发布之前没有看到的这个问题(). That one also suggested creating a decorator around the HttpServletRequest, so I adapted the info from http://www.myjavarecipes.com/how-to-read-post-request-data-twice-in-spring/,添加针对大请求的保护。
以下是我的想法,以防有人有任何反馈意见:
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
// We include a max byte size to protect against malicious requests, since this all has to be read into memory
public static final Integer MAX_BYTE_SIZE = 1_048_576; // 1 MB
private String _body;
public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
_body = "";
InputStream bounded = new BoundedInputStream(request.getInputStream(), MAX_BYTE_SIZE);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bounded));
String line;
while ((line = bufferedReader.readLine()) != null){
_body += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
return new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
我使用了以下配置:
@Bean
FilterRegistrationBean multiReadFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
MultiReadRequestFilter multiReadRequestFilter = new MultiReadRequestFilter();
registrationBean.setFilter(multiReadRequestFilter);
registrationBean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER - 2);
registrationBean.setUrlPatterns(Sets.newHashSet("/path/here"));
return registrationBean;
}
这个问题是我使用 Spring Security Oauth2 库进行的一些工作的结果。我已经设置了一个 oauth2 授权服务器和一个 oauth2 资源服务器,后者旨在根据访问令牌进行授权。
问题是通常访问令牌是在 header 中传递的,但是我们为其设置的大客户想要在 JSON 请求中传递访问令牌 body.您可以使用一个界面来设置自定义访问令牌提取,但它看起来像这样:
public interface TokenExtractor {
/**
* Extract a token value from an incoming request without authentication.
*
* @param request the current ServletRequest
* @return an authentication token whose principal is an access token (or null if there is none)
*/
Authentication extract(HttpServletRequest request);
}
因此,据我所知,我只能访问原始 HTTPServletRequest,我需要从中反序列化请求并提取访问令牌。
然而,更复杂的是请求 body 还包含处理所需的其他参数,因此我想将其反序列化为传递到我的控制器中的 DTO class ,像这样:
@RequestMapping("/oauth/someresource")
@Transactional
public Map<String, String> resource(@AuthenticationPrincipal UserDetails userDetails,
@RequestBody ClientRequestDto clientRequestDto) {
// Do some processing based on the request dto
}
我尝试在令牌提取器中手动反序列化请求,但随后出现错误 "java.lang.IllegalStateException: getReader() has already been called for this request"。
我正在集思广益,想出一些我可以研究的可能解决方案,到目前为止我已经想出了:
- 想办法重置输入流
- 反序列化令牌提取器中的 object,将其附加到原始请求 object,然后在我的控制器中访问原始请求 object 而不是使用 @RequestBody
- 与 2 类似,但找到一种方法来添加自定义反序列化器,该反序列化器获取附加到原始请求的 object 而不是处理请求的输入流。
无论如何,这些只是一些想法,如果有人对解决此问题的优雅方法有任何想法,我将不胜感激。
编辑:我确实发现了这个类似的问题:
所以我最终找到了另一个问题来解决我在发布之前没有看到的这个问题(
以下是我的想法,以防有人有任何反馈意见:
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
// We include a max byte size to protect against malicious requests, since this all has to be read into memory
public static final Integer MAX_BYTE_SIZE = 1_048_576; // 1 MB
private String _body;
public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
_body = "";
InputStream bounded = new BoundedInputStream(request.getInputStream(), MAX_BYTE_SIZE);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bounded));
String line;
while ((line = bufferedReader.readLine()) != null){
_body += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
return new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
我使用了以下配置:
@Bean
FilterRegistrationBean multiReadFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
MultiReadRequestFilter multiReadRequestFilter = new MultiReadRequestFilter();
registrationBean.setFilter(multiReadRequestFilter);
registrationBean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER - 2);
registrationBean.setUrlPatterns(Sets.newHashSet("/path/here"));
return registrationBean;
}