为什么我的 spring 引导无状态过滤器被调用两次?
Why is my spring boot stateless filter being called twice?
我正在尝试实现基于令牌的无状态身份验证 api 我使用 Spring Boot.这个想法是客户端在任何请求中都包含一个 JWT 令牌,过滤器从请求中提取它,并根据令牌的内容使用相关的 Authentication 对象设置 SecurityContext。然后请求被正常路由,并在映射方法上使用 @PreAuthorize 进行保护。
我的安全配置如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTTokenAuthenticationService authenticationService;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(new StatelessAuthenticationFilter(authenticationService), UsernamePasswordAuthenticationFilter.class);
}
使用扩展 GenericFilterBean 的无状态过滤器定义如下:
public class StatelessAuthenticationFilter extends GenericFilterBean {
private static Logger logger = Logger.getLogger(StatelessAuthenticationFilter.class);
private JWTTokenAuthenticationService authenticationservice;
public StatelessAuthenticationFilter(JWTTokenAuthenticationService authenticationService)
{
this.authenticationservice = authenticationService;
}
@Override
public void doFilter( ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
Authentication authentication = authenticationservice.getAuthentication(httpRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.info("===== Security Context before request =====");
logger.info("Request for: " + httpRequest.getRequestURI());
logger.info(SecurityContextHolder.getContext().getAuthentication());
logger.info("===========================================");
chain.doFilter(request, response);
SecurityContextHolder.getContext().setAuthentication(null);
logger.info("===== Security Context after request =====");
logger.info("Request for: " + httpRequest.getRequestURI());
logger.info(SecurityContextHolder.getContext().getAuthentication());
logger.info("===========================================");
}
}
端点定义如下:
@PreAuthorize("hasAuthority('user')")
@RequestMapping ( value="/api/attachments/{attachmentId}/{fileName:.+}",
method = RequestMethod.GET)
public ResponseEntity<byte[]> getAttachedDocumentEndpoint(@PathVariable String attachmentId, @PathVariable String fileName)
{
logger.info("GET called for /attachments/" + attachmentId + "/" + fileName);
// do something to get the file, and return ResponseEntity<byte[]> object
}
在 /api/attachments/someattachment/somefilename 上执行 GET 时,包括令牌,我可以看到过滤器被调用了两次,一次显然是使用令牌,一次没有。但是映射到request的restcontroller只调用了一次
[INFO] [06-04-2015 12:26:44,465] [JWTTokenAuthenticationService] getAuthentication - Getting authentication based on token supplied in HTTP Header
[INFO] [06-04-2015 12:26:44,473] [StatelessAuthenticationFilter] doFilter - ===== Security Context before request =====
[INFO] [06-04-2015 12:26:44,473] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,474] [StatelessAuthenticationFilter] doFilter - Name:iser, Principal:user, isAuthenticated:true, grantedAuthorites:[user]
[INFO] [06-04-2015 12:26:44,474] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,476] [AttachmentRESTController] getAttachedDocumentEndpoint - GET called for /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,477] [AttachmentDBController] getAttachment - getAttachment method called with attachmentId:1674b08b6bbd54a6efaff4a780001a9e , and fileName:jpg.png
[INFO] [06-04-2015 12:26:44,483] [StatelessAuthenticationFilter] doFilter - ===== Security Context after request =====
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,507] [JWTTokenAuthenticationService] getAuthentication - No token supplied in HTTP Header
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter - ===== Security Context before request =====
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===== Security Context after request =====
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===========================================
这是怎么回事?
编辑:
它甚至比我最初想象的还要奇怪 - 实现一个简单的端点,仅 return 一条简单的消息显示预期的行为 - 似乎只有当我尝试 return 数据作为 ResponseEntity 作为以上是否出现此问题。
端点:
@PreAuthorize("hasAuthority('user')")
@RequestMapping("/api/userHelloWorld")
public String userHelloWorld()
{
return "Hello Secure User World";
}
输出,显示对过滤器的单次调用(启用额外调试):
[INFO] [06-04-2015 19:43:25,831] [JWTTokenAuthenticationService] getAuthentication - Getting authentication based on token supplied in HTTP Header
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - ===== Security Context before request =====
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Request for: /api/userHelloWorld
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Response = null 200
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Name:user, Principal:user, isAuthenticated:true, grantedAuthorites:[user]
[INFO] [06-04-2015 19:43:25,845] [StatelessAuthenticationFilter] doFilterInternal - ===========================================
[DEBUG] [06-04-2015 19:43:25,845] [DispatcherServlet] doService - DispatcherServlet with name 'dispatcherServlet' processing GET request for [/api/userHelloWorld]
[DEBUG] [06-04-2015 19:43:25,847] [AbstractHandlerMethodMapping] getHandlerInternal - Looking up handler method for path /api/userHelloWorld
[DEBUG] [06-04-2015 19:43:25,848] [AbstractHandlerMethodMapping] getHandlerInternal - Returning handler method [public java.lang.String RESTController.userHelloWorld()]
[DEBUG] [06-04-2015 19:43:25,849] [DispatcherServlet] doDispatch - Last-Modified value for [/api/userHelloWorld] is: -1
[DEBUG] [06-04-2015 19:43:25,851] [AbstractMessageConverterMethodProcessor] writeWithMessageConverters - Written [Hello Secure User World] as "text/plain;charset=UTF-8" using [org.springframework.http.converter.StringHttpMessageConverter@3eaf6fe7]
[DEBUG] [06-04-2015 19:43:25,852] [DispatcherServlet] processDispatchResult - Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
[DEBUG] [06-04-2015 19:43:25,852] [FrameworkServlet] processRequest - Successfully completed request
[INFO] [06-04-2015 19:43:25,852] [StatelessAuthenticationFilter] doFilterInternal - ===== Security Context after request =====
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - Request for: /api/userHelloWorld
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - Response = text/plain;charset=UTF-8 200
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal -
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - ===========================================
它是过滤器的一部分,对我来说仍然有些黑魔法 (*),但我知道这是一个常见问题,并且 Spring 有一个子class GenericFilterBean
专门用于处理它:只需使用 OncePerRequestFilter
作为基础 class 并且你的过滤器应该只被调用一次。
(*) 我读过这可能是由于通过请求调度程序多次调度请求引起的
好的 - 所以这很荒谬,但这似乎是我调用请求的方式的问题(通过 POSTMAN Chrome 扩展)
Postman 似乎触发了 2 个请求,一个有 headers,一个没有。这里有一个公开的错误报告对此进行了描述:
https://github.com/a85/POSTMan-Chrome-Extension/issues/615
如果使用 curl 或直接从浏览器调用请求,则看不到该行为。
我正在尝试实现基于令牌的无状态身份验证 api 我使用 Spring Boot.这个想法是客户端在任何请求中都包含一个 JWT 令牌,过滤器从请求中提取它,并根据令牌的内容使用相关的 Authentication 对象设置 SecurityContext。然后请求被正常路由,并在映射方法上使用 @PreAuthorize 进行保护。
我的安全配置如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTTokenAuthenticationService authenticationService;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(new StatelessAuthenticationFilter(authenticationService), UsernamePasswordAuthenticationFilter.class);
}
使用扩展 GenericFilterBean 的无状态过滤器定义如下:
public class StatelessAuthenticationFilter extends GenericFilterBean {
private static Logger logger = Logger.getLogger(StatelessAuthenticationFilter.class);
private JWTTokenAuthenticationService authenticationservice;
public StatelessAuthenticationFilter(JWTTokenAuthenticationService authenticationService)
{
this.authenticationservice = authenticationService;
}
@Override
public void doFilter( ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
Authentication authentication = authenticationservice.getAuthentication(httpRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.info("===== Security Context before request =====");
logger.info("Request for: " + httpRequest.getRequestURI());
logger.info(SecurityContextHolder.getContext().getAuthentication());
logger.info("===========================================");
chain.doFilter(request, response);
SecurityContextHolder.getContext().setAuthentication(null);
logger.info("===== Security Context after request =====");
logger.info("Request for: " + httpRequest.getRequestURI());
logger.info(SecurityContextHolder.getContext().getAuthentication());
logger.info("===========================================");
}
}
端点定义如下:
@PreAuthorize("hasAuthority('user')")
@RequestMapping ( value="/api/attachments/{attachmentId}/{fileName:.+}",
method = RequestMethod.GET)
public ResponseEntity<byte[]> getAttachedDocumentEndpoint(@PathVariable String attachmentId, @PathVariable String fileName)
{
logger.info("GET called for /attachments/" + attachmentId + "/" + fileName);
// do something to get the file, and return ResponseEntity<byte[]> object
}
在 /api/attachments/someattachment/somefilename 上执行 GET 时,包括令牌,我可以看到过滤器被调用了两次,一次显然是使用令牌,一次没有。但是映射到request的restcontroller只调用了一次
[INFO] [06-04-2015 12:26:44,465] [JWTTokenAuthenticationService] getAuthentication - Getting authentication based on token supplied in HTTP Header
[INFO] [06-04-2015 12:26:44,473] [StatelessAuthenticationFilter] doFilter - ===== Security Context before request =====
[INFO] [06-04-2015 12:26:44,473] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,474] [StatelessAuthenticationFilter] doFilter - Name:iser, Principal:user, isAuthenticated:true, grantedAuthorites:[user]
[INFO] [06-04-2015 12:26:44,474] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,476] [AttachmentRESTController] getAttachedDocumentEndpoint - GET called for /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,477] [AttachmentDBController] getAttachment - getAttachment method called with attachmentId:1674b08b6bbd54a6efaff4a780001a9e , and fileName:jpg.png
[INFO] [06-04-2015 12:26:44,483] [StatelessAuthenticationFilter] doFilter - ===== Security Context after request =====
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,484] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,507] [JWTTokenAuthenticationService] getAuthentication - No token supplied in HTTP Header
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter - ===== Security Context before request =====
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,507] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===========================================
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===== Security Context after request =====
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - Request for: /api/attachments/1674b08b6bbd54a6efaff4a780001a9e/jpg.png
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter -
[INFO] [06-04-2015 12:26:44,508] [StatelessAuthenticationFilter] doFilter - ===========================================
这是怎么回事?
编辑:
它甚至比我最初想象的还要奇怪 - 实现一个简单的端点,仅 return 一条简单的消息显示预期的行为 - 似乎只有当我尝试 return 数据作为 ResponseEntity 作为以上是否出现此问题。
端点:
@PreAuthorize("hasAuthority('user')")
@RequestMapping("/api/userHelloWorld")
public String userHelloWorld()
{
return "Hello Secure User World";
}
输出,显示对过滤器的单次调用(启用额外调试):
[INFO] [06-04-2015 19:43:25,831] [JWTTokenAuthenticationService] getAuthentication - Getting authentication based on token supplied in HTTP Header
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - ===== Security Context before request =====
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Request for: /api/userHelloWorld
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Response = null 200
[INFO] [06-04-2015 19:43:25,844] [StatelessAuthenticationFilter] doFilterInternal - Name:user, Principal:user, isAuthenticated:true, grantedAuthorites:[user]
[INFO] [06-04-2015 19:43:25,845] [StatelessAuthenticationFilter] doFilterInternal - ===========================================
[DEBUG] [06-04-2015 19:43:25,845] [DispatcherServlet] doService - DispatcherServlet with name 'dispatcherServlet' processing GET request for [/api/userHelloWorld]
[DEBUG] [06-04-2015 19:43:25,847] [AbstractHandlerMethodMapping] getHandlerInternal - Looking up handler method for path /api/userHelloWorld
[DEBUG] [06-04-2015 19:43:25,848] [AbstractHandlerMethodMapping] getHandlerInternal - Returning handler method [public java.lang.String RESTController.userHelloWorld()]
[DEBUG] [06-04-2015 19:43:25,849] [DispatcherServlet] doDispatch - Last-Modified value for [/api/userHelloWorld] is: -1
[DEBUG] [06-04-2015 19:43:25,851] [AbstractMessageConverterMethodProcessor] writeWithMessageConverters - Written [Hello Secure User World] as "text/plain;charset=UTF-8" using [org.springframework.http.converter.StringHttpMessageConverter@3eaf6fe7]
[DEBUG] [06-04-2015 19:43:25,852] [DispatcherServlet] processDispatchResult - Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
[DEBUG] [06-04-2015 19:43:25,852] [FrameworkServlet] processRequest - Successfully completed request
[INFO] [06-04-2015 19:43:25,852] [StatelessAuthenticationFilter] doFilterInternal - ===== Security Context after request =====
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - Request for: /api/userHelloWorld
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - Response = text/plain;charset=UTF-8 200
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal -
[INFO] [06-04-2015 19:43:25,853] [StatelessAuthenticationFilter] doFilterInternal - ===========================================
它是过滤器的一部分,对我来说仍然有些黑魔法 (*),但我知道这是一个常见问题,并且 Spring 有一个子class GenericFilterBean
专门用于处理它:只需使用 OncePerRequestFilter
作为基础 class 并且你的过滤器应该只被调用一次。
(*) 我读过这可能是由于通过请求调度程序多次调度请求引起的
好的 - 所以这很荒谬,但这似乎是我调用请求的方式的问题(通过 POSTMAN Chrome 扩展)
Postman 似乎触发了 2 个请求,一个有 headers,一个没有。这里有一个公开的错误报告对此进行了描述: https://github.com/a85/POSTMan-Chrome-Extension/issues/615
如果使用 curl 或直接从浏览器调用请求,则看不到该行为。