【问题标题】:Why is my spring boot stateless filter being called twice?为什么我的 spring boot 无状态过滤器被调用了两次?
【发布时间】:2015-08-19 00:27:14
【问题描述】:

我正在尝试在我使用 Spring Boot 开发的 rest api 上实现基于无状态令牌的身份验证。这个想法是客户端在任何请求中都包含一个 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 时,我可以看到过滤器被调用了两次,一次显然带有令牌,一次没有。但是映射到请求的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 - ===========================================

这是怎么回事?

编辑:

这比我最初想象的还要奇怪 - 实现一个只返回简单消息的简单端点会显示预期的行为 - 似乎只有当我像上面那样尝试将数据作为 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 - ===========================================

【问题讨论】:

  • 我有类似的东西。我使用 Grails 应用程序和 Spring Rest 作为后端。以及一个 Angular 应用程序作为另一台服务器上的前端。从 Angular 执行 http.post 时,在 Grails 中,我的 OncePerRequestFilter 中的 doFilterInternal 方法被调用了两次。在阅读http://www.tutorialspoint.com/http/http_methods.htm 之后,我觉得浏览器首先使用 OPTIONS 请求来找出服务器支持的方法是有意义的。然后发送 POST 请求。

标签: rest spring-security spring-boot restful-authentication jwt


【解决方案1】:

好的 - 这很荒谬,但我调用请求的方式似乎有问题(通过 POSTMAN Chrome 扩展)

邮递员似乎触发了 2 个请求,一个带有标头,一个没有标头。有一个开放的错误报告描述了这里: https://github.com/a85/POSTMan-Chrome-Extension/issues/615

如果使用 curl 或直接从浏览器调用请求,则不会看到该行为。

【讨论】:

  • 我尝试了 curl,它也触发了 2 个请求。正如 Serge Ballesta 建议的那样,我使用了 OncePerRequestFilter 并且过滤器只被调用一次。
【解决方案2】:

它是过滤器的一部分,对我来说仍然有一些黑魔法(*),但我知道这是一个常见问题,Spring 有一个GenericFilterBean 的子类专门用于处理它:只需使用@987654322 @ 作为基类,你的过滤器应该只被调用一次。

(*) 我读过这可能是由于请求通过请求调度程序多次调度造成的

【讨论】:

  • 感谢这个 Serge,但即使将过滤器更改为 OncePerRequestFilter 的子类也无济于事 - 我仍然看到对过滤器的额外调用。
猜你喜欢
  • 2010-10-23
  • 2011-09-04
  • 2021-01-22
  • 1970-01-01
  • 2013-12-20
  • 2012-08-02
  • 2015-11-18
  • 2019-06-06
  • 1970-01-01
相关资源
最近更新 更多