[spring-projects/spring-boot]ResponseStatusException 在 2.6.1 中不再使用 spring security 返回响应主体

2024-05-14 168 views
4

我们发现升级到 spring boot 2.6.1 后,即使对于经过身份验证的用户,也不再填充 ResponseStatusException 的响应正文

看起来 Spring Boot 转发到 /error 页面,但是扩展了 OncePerRequestFilter 的BearerTokenAuthenticationFIlter在错误状态下不会向 Spring 安全上下文添加必要的身份验证。这意味着我们然后点击#26356并且正文是空的。

示例项目是https://github.com/ministryofjustice/hmpps-spring-boot-2.6-bug

如果你运行 ./gradlew test 那么它会失败

分支https://github.com/ministryofjustice/hmpps-spring-boot-2.6-bug/tree/previous-working-version显示它在 Spring Boot 2.5.6 的先前版本中工作,交替白名单/错误也修复了它(但我们不想将其列入白名单/错误)

回答

6

感谢您提供样品。

您观察到的行为是针对 Spring Boot 2.6 的。您的安全配置需要身份验证才能访问除/favicon.ico/csrf/health/**/info/webjars/**和 之外的所有内容/v2/api-docs。这意味着您已经保护了 Spring Boot 的错误页面/error,并且由于 #26356 中的更改,未经身份验证的用户不再收到完整的错误响应。

如果您希望任何用户(无论是否经过身份验证)接收 Spring Boot 的默认错误响应,您应该允许访问/error.将此差异应用到您的应用程序可以修复失败的测试:

diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/token/config/ResourceServerConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/token/config/ResourceServerConfiguration.kt
index aa77ab2..6a4d0a6 100644
--- a/src/main/kotlin/uk/gov/justice/digital/hmpps/token/config/ResourceServerConfiguration.kt
+++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/token/config/ResourceServerConfiguration.kt
@@ -25,7 +25,7 @@ open class ResourceServerConfiguration : WebSecurityConfigurerAdapter() {
       .authorizeRequests { auth ->
         auth.antMatchers(
           "/favicon.ico", "/csrf", "/health/**", "/info",
-          "/webjars/**", "/v2/api-docs"
+          "/webjars/**", "/v2/api-docs", "/error"
         )
           .permitAll().anyRequest().authenticated()
       }
1

我认为 2.6 中重要的行为变化是,如果应用程序依赖于默认错误处理程序,经过身份验证的用户将不再收到完整的错误响应。最好记录此更改并提供推荐的解决方案。

确实感觉有点奇怪,因为由于没有提供凭据时可以访问错误页面,所以我们现在最终得到了提供凭据时无法访问错误页面,并且该问题的建议解决方案是允许访问错误页面给大家!

因此,听起来确实不应该使用默认的错误处理程序。相反,我猜应用程序可以捕获异常并ResponseEntity直接返回,或者抛出自定义异常,然后实现@ExceptionHandler返回ResponseEntity.

0

我认为 2.6 中重要的行为变化是,如果应用程序依赖于默认错误处理程序,经过身份验证的用户将不再收到完整的错误响应。

不应该是这样的。

该问题的推荐解决方案是允许每个人都可以访问错误页面顶部

这不是建议。如果用户通过身份验证,他们应该在错误响应中收到正文,而不允许所有人访问错误页面。如果这种情况没有发生,那么我们需要进一步调查。可能涉及到一个错误的过滤器(我相信您上面链接的第二个问题就是这种情况),您遇到了Spring Security 的限制或其他问题。

5

上面示例项目中的用户已完全通过身份验证,但他们没有收到错误响应中的正文。

看起来 Spring Boot 转发到 /error 页面,但是扩展了 OncePerRequestFilter 的BearerTokenAuthenticationFIlter在错误状态下不会向 Spring 安全上下文添加必要的身份验证。

因此,我应该针对 BearerTokenAuthenticationFIlter 提出错误吗? (我认为其他过滤器也会受到影响)

8

抱歉,@SimonMitchellMOJ,我误解了示例所演示的问题。

该问题是由于 Spring SecuritySecurityContextPersistenceFilter根据所使用的身份验证方法而表现不同。使用基本身份验证时,当请求失败时,将恢复初始请求的身份验证,并转发到 Boot 的错误页面:

2021-12-10 10:11:08.830 DEBUG 5568 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /fail
2021-12-10 10:11:08.835 DEBUG 5568 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2021-12-10 10:11:09.002 DEBUG 5568 --- [o-auto-1-exec-1] o.s.s.a.dao.DaoAuthenticationProvider    : Authenticated user
2021-12-10 10:11:09.003 DEBUG 5568 --- [o-auto-1-exec-1] o.s.s.w.a.www.BasicAuthenticationFilter  : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=username, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]
2021-12-10 10:11:09.023 DEBUG 5568 --- [o-auto-1-exec-1] w.c.HttpSessionSecurityContextRepository : Created HttpSession as SecurityContext is non-default
2021-12-10 10:11:09.023 DEBUG 5568 --- [o-auto-1-exec-1] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=username, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@651af14a]
2021-12-10 10:11:09.028 DEBUG 5568 --- [o-auto-1-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [GET /fail] with attributes [fullyAuthenticated]
2021-12-10 10:11:09.029 DEBUG 5568 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Secured GET /fail
2021-12-10 10:11:09.048 DEBUG 5568 --- [o-auto-1-exec-1] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=username, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@651af14a]
2021-12-10 10:11:09.048 DEBUG 5568 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2021-12-10 10:11:09.059 DEBUG 5568 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /error
2021-12-10 10:11:09.059 DEBUG 5568 --- [o-auto-1-exec-1] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=username, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]]
2021-12-10 10:11:09.059 DEBUG 5568 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=username, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]]
2021-12-10 10:11:09.059 DEBUG 5568 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Secured GET /error
2021-12-10 10:11:09.120 DEBUG 5568 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

相比之下,当使用不记名令牌时,转发/error是通过匿名身份验证完成的,而不是AuthAwareAuthenticationToken最初建立的身份验证:

2021-12-10 10:09:46.611 DEBUG 5524 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Securing POST /token/verify
2021-12-10 10:09:46.615 DEBUG 5524 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2021-12-10 10:09:46.645 DEBUG 5524 --- [o-auto-1-exec-1] o.s.s.o.s.r.a.JwtAuthenticationProvider  : Authenticated token
2021-12-10 10:09:46.646 DEBUG 5524 --- [o-auto-1-exec-1] .o.s.r.w.BearerTokenAuthenticationFilter : Set SecurityContextHolder to AuthAwareAuthenticationToken [Principal=token-verification-api-client, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[SCOPE_read, SCOPE_write]]
2021-12-10 10:09:46.651 DEBUG 5524 --- [o-auto-1-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [POST /token/verify] with attributes [authenticated]
2021-12-10 10:09:46.652 DEBUG 5524 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Secured POST /token/verify
2021-12-10 10:09:46.707 DEBUG 5524 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2021-12-10 10:09:46.711 DEBUG 5524 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Securing POST /error
2021-12-10 10:09:46.711 DEBUG 5524 --- [o-auto-1-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2021-12-10 10:09:46.712 DEBUG 5524 --- [o-auto-1-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2021-12-10 10:09:46.712 DEBUG 5524 --- [o-auto-1-exec-1] o.s.security.web.FilterChainProxy        : Secured POST /error

新引入的功能ErrorPageSecurityFilter是建立在这样的假设之上的:Spring Security 在转发请求时可用的身份验证方面将表现一致。上面的内容表明,使用不记名令牌时,该假设并不成立。我们需要与安全团队讨论这个问题,以确定最佳的行动方案。

3

我在我们的应用程序中看到了同样的问题。我们写了我们自己的习惯Authentication。我们也不再看到对我们的请求的响应正文。因为2.5.7它工作得很好。
归根结底是因为我发现的情况ErrorController.error(HttpServletRequest request)没有被叫进来。2.6.1我尝试了当前的快照,2.6.2结果相同,没有响应正文。我尝试添加/error到允许的路径列表中,但这也没有带回响应正文。
它的中继很不幸,因为这对我们来说是一个更新拦截器。特别是对于新的 log4j cve,如果 2.6.2 发布后能够立即升级,那就太好了(当然,我们通过在 gradle 文件中强制 log4j 为版本 2.15.0 来暂时修复它)

4

@cmdjulian 您能否提供一个我们可以用来复制问题的最小示例?您很可能遇到同样的问题,但我想确定一下,因为您提到了自定义,Authentication而原始问题是由于.AuthenticationSTATELESS

5

我们SessionCreationPolicy.STATELESS也用。我们创建了一个自定义Authentication实现来丰富SecurityContext更多数据。在底层,它只是提取给Authentication定请求标头中存在的 JWT。
代码可以是图片如下:

public class JwtAuthorizationFilter extends OncePerRequestFilter {

    public static final String BEARER_TOKEN_PREFIX = "Bearer ";

    /**
     * Put token into Spring Security Context, so that {@link JwtAuthenticationProvider}
     * can verify the tokens validity.
     */
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest req, @NonNull HttpServletResponse res, FilterChain chain)
        throws IOException, ServletException {

        getJwtFromRequest(req).ifPresent(jwtCredentials -> {
            JwtAuthenticationToken jwtAuthenticationToken = JwtAuthenticationToken.builder()
                .token(jwtCredentials)
                .details(new WebAuthenticationDetailsSource().buildDetails(req))
                .authenticated(false)
                .build();

            var context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(jwtAuthenticationToken);
            SecurityContextHolder.setContext(context);
        });

        chain.doFilter(req, res);
    }

    /**
     * Extract JWT value from Request header.
     */
    private Optional<String> getJwtFromRequest(HttpServletRequest request) {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);

        return StringUtils.startsWith(token, BEARER_TOKEN_PREFIX)
            ? Optional.of(token.substring(BEARER_TOKEN_PREFIX.length()))
            : Optional.empty();
    }

}
@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final JwtManager manager;

    private final UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        final VerifiedUserToken verifiedToken = manager.verifyToken(authentication.getCredentials().toString());

        return authenticateUser(verifiedToken, authentication.getDetails());
    }

    private Authentication authenticateUser(VerifiedUserToken token, Object details) {
        UserDetails user = userDetailsService.loadUserByUsername(token.getUsername());

        return constructAuthentication(token.getRawToken(), user, details);
    }

    private Authentication constructAuthentication(String token, UserDetails user, Object details) {
        return JwtAuthenticationToken.builder()
            .token(token)
            .user(user)
            .details(details)
            .authorities(user.getAuthorities())
            .authenticated(true)
            .build();
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(JwtAuthenticationToken.class);
    }

}

只是JwtAuthenticationToken实现Authentication没有任何额外的嗡嗡声。如果您仍然希望我提交完整的示例,请告诉我。
除了不使用 Spring OAuth2 之外,它与提供的示例中的示例相同

7

从 2.5.x 升级到 2.6.1 的应用程序中是否有可能的(临时)解决方法,或者我们几乎一直在等待 2.6.2?

5

@mjustin 解决方法是删除此评论ErrorPageSecurityFilter中所述的。

6

@mjustin 解决方法是删除此评论ErrorPageSecurityFilter中所述的。

谢谢。但是,当用户未通过身份验证登录时,这是否会产生启用错误页面的副作用?

6

@mbhave 我刚刚更新到2.6.2,行为是一样的...我不确定 d9d161c 是否真的完全解决了这个问题。问题(对我来说)似乎是ErrorPageSecurityFilter.java#L80 - 下面的代码片段:

private boolean isAllowed(HttpServletRequest request, Integer errorCode) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (isUnauthenticated(authentication) && isNotAuthenticationError(errorCode)) {
        return true;
    }
    return getPrivilegeEvaluator().isAllowed(request.getRequestURI(), authentication);
}

即使authentication对于经过身份验证的用户来说,它始终是一个实例AnonymousAuthenticationToken- 在我的场景中,用户试图访问他们不允许查看的资源。其中,我期待 HTTP 403 响应,但我也希望看到错误正文(因为用户通过身份验证)。即使设置也.antMatchers("/error").permitAll()不能解决问题。为何被authentication消灭SecurityContextHolder

4

我想说这解决了我遇到的具体问题,所以它看起来至少是部分修复。

3

@mbhave 我刚刚更新到2.6.2,行为是一样的...我不确定d9d161c是否真的完全解决了这个问题。问题(对我来说)似乎是ErrorPageSecurityFilter.java#L80 - 下面的代码片段:

private boolean isAllowed(HttpServletRequest request, Integer errorCode) {
  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  if (isUnauthenticated(authentication) && isNotAuthenticationError(errorCode)) {
      return true;
  }
  return getPrivilegeEvaluator().isAllowed(request.getRequestURI(), authentication);
}

即使authentication对于经过身份验证的用户来说,它始终是一个实例AnonymousAuthenticationToken- 在我的场景中,用户试图访问他们不允许查看的资源。其中,我期待 HTTP 403 响应,但我也希望看到错误正文(因为用户通过身份验证)。即使设置也.antMatchers("/error").permitAll()不能解决问题。为何被authentication消灭SecurityContextHolder

@mbhave @datagitlies 我们有相同的场景:如果用户无权访问(403)资源,则显示(自定义)Spring Boot 错误页面。据我所知,当前的ErrorPageSecurityFilter实现不支持这种情况。

WebInvocationPrivilegeEvaluator.isAllowed(uri, authentication)HTTP 401/情况下的实现调用403

在调试过程中我观察到以下情况:

  1. 用作方法的参数,ErrorPageSecurityFilter其中包含上下文路径。 JavaDoc指出参数中应排除上下文路径HttpServletRequest.getRequestURI()uriWebInvocationPrivilegeEvaluator.isAllowed(uri, authentication)uri

.antMatchers("/error").permitAll()这似乎是在这种情况下类似匹配器不匹配的原因。相反,匹配器必须另外包含要匹配的上下文路径。

  1. 就像 @datagitlies 已经描述的那样,如果无法访问实际的authentication(在@Transient身份验证或STATELESS会话策略的情况下)匹配器,则.antMatchers("/error").authenticated()无法工作。

这些是已知的限制,还是所描述的情况不打算通过使用 spring 错误页面来实现?

7

@lgraf我相信总是.antMatcher(...)需要模式中的上下文路径。您可能想尝试mvcMatcher(...)一下。如果您仍然遇到您认为是由错误引起的问题,您可以打开一个新问题并附加一个我们可以运行和调试的示例项目。