【问题标题】:What is Spring's SecurityContext behavior in terms of threads or different requests?就线程或不同请求而言,Spring 的 SecurityContext 行为是什么?
【发布时间】:2021-07-29 03:10:05
【问题描述】:

我正在经历 Spring Security 的不同类实现。我知道我们将 Authentication 对象设置为 SecurityContext ThreadLocal 对象为:

UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

upat.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(upat);

因此,基本上每个线程都有一个单独的 SecurityContext ThreadLocal 对象副本,该对象保存该线程的 Authentication 对象。到这里为止很好。我在我的 SecurityConfiguration 中也将 SessionCreationPolicy 设置为无状态。下面是安全配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        final CorsConfigurer<HttpSecurity> cors = http.csrf().disable().cors().configurationSource(source);

        final ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry exp =
                cors.and().authorizeRequests();

        exp.antMatchers("/getJWTToken/**").permitAll()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/rest/**").authenticated();

        exp.and().exceptionHandling()
                .authenticationEntryPoint(authEntryPoint())
                .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        ;

        // Add a filter to validate the tokens with every request
        http.addFilterBefore(authRequestFilter(), UsernamePasswordAuthenticationFilter.class);
    }

但是,我对“线程”在这里的含义感到困惑?

  1. 他们的意思是,单个 HTTP 请求与会话没有任何关系,即对于每个 HTTP 请求,都会有一个新的 ThreadLocal Authentication 对象?
  2. 或者,它是特定于 HTTP 会话的吗?即对于用户的会话,将只有一个线程,因此只有一个安全上下文?

对于上述两点,我也有这两个疑问。

  1. 对于上面的1,如果它随着每个请求而改变,那么为什么我们需要在每个请求的线程中检查Authentication对象,如下所示。我的意思是,如果它是一个不同的线程,则不需要这个。它肯定是空的。 (我所指的应用程序中存在以下 if 条件)。
if( SecurityContextHolder.getContext().getAuthentication() == null ) {
    if( jwtTokenUtil.validateToken(jwtToken, userObj) )
    {
        if( userObj == null )
        {
            response.setStatus(401);
            return;
        }
        else
        {
            UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(userObj, null,userObj.getAuthorities());

            upat.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // After setting the Authentication in the context, we specify
            // that the current user is authenticated. So it passes the
            // Spring Security Configurations successfully.
            SecurityContextHolder.getContext().setAuthentication(upat);
        }
    }
}
  1. 对于上述 2,如果我在 Security Config 类中将 SessionCreationPolicy 设置为无状态,则再次没有会话,但不同线程上的请求不同。

我在这里对线程(ThreadLocal SecurityContext)的解释可能是错误的。 需要帮助。

【问题讨论】:

    标签: java spring spring-boot spring-security web-applications


    【解决方案1】:

    SecurityContextHolder, SecurityContext and Authentication Objects

    默认情况下,SecurityContextHolder 使用ThreadLocal 来存储这些详细信息,这意味着安全上下文始终可用于同一执行线程中的方法。以这种方式使用ThreadLocalquite safe if care is taken to clear the thread after the present principal’s request is processed。当然,Spring Security 会自动为您处理这些,因此您无需担心。

    某些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的特定方式。例如,Swing 客户端可能希望 Java 虚拟机中的所有线程使用相同的安全上下文。 SecurityContextHolder 可以在启动时配置策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL 策略。其他应用程序可能希望安全线程产生的线程也采用相同的安全身份。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 来实现的。您可以通过两种方式从默认的SecurityContextHolder.MODE_THREADLOCAL 更改模式。

    第一个是设置系统属性,第二个是在SecurityContextHolder上调用静态方法。大多数应用程序不需要更改默认设置,但如果您这样做,请查看 JavaDoc for SecurityContextHolder 以了解更多信息。


    Storing the SecurityContext between requests

    在 Spring Security 中,存储请求之间的 SecurityContext 的责任落在了 SecurityContextPersistenceFilter 上,它默认将上下文存储为 HTTP 请求之间的 HttpSession 属性。它将每个请求的上下文恢复到 SecurityContextHolder,并且至关重要的是,在请求完成时清除 SecurityContextHolder

    许多其他类型的应用程序(例如,无状态 RESTful 网络服务)不使用 HTTP 会话,并且会在每个请求上重新进行身份验证。但是,将SecurityContextPersistenceFilter 包含在链中以确保在每次请求后清除SecurityContextHolder 仍然很重要。


    会话管理

    .sessionManagement()
         .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    

    将导致 Spring Security 使用NullSecurityContextRepository,而不是默认的HttpSessionSecurityContextRepository

    这是一个简单的实现,因为它根本不会向 HTTP 会话保存任何内容,并且对于每个请求,创建一个全新的空 SecurityContext,因此没有存储身份验证等。


    更新

    这意味着,如果会话策略是,则以下条件始终为真 无国籍。 if(SecurityContextHolder.getContext().getAuthentication() == 空)

    是的,除非您在条件被调用之前将其设置为 null,否则您将获得身份验证。如果您使用的是 JWT 令牌,您可以进行如下验证并设置安全上下文。

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        String jwt = resolveToken(httpServletRequest);
    
        if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {
            Authentication authentication = this.tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            ...
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    
    private String resolveToken(HttpServletRequest request){
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
    

    【讨论】:

    • 也就是说,如果会话策略是无状态的,则以下条件始终为真。 if(SecurityContextHolder.getContext().getAuthentication() == null)
    • 你好 Ashish,我已经添加了更多细节。
    • 好的,谢谢。因此,如果 SecurityContext 不为 null,那么一旦请求通过跳过 if 条件到达 UsernamePasswordAuthenticationFilter,具有 Bearer 令牌的传入请求将使用 SecurityContextHolder 提供的 Authentication 对象进行身份验证。如果是这样,UsernamePasswordAuthenticationFilter 将如何对其进行身份验证。或者,UsernamePasswordAuthenticationFilter 会查看SecurityContextHolder 中是否有Authentication 对象,它会自动认为它已通过身份验证吗?并且,将请求提前发送到链中剩余的过滤器。
    • 理想情况下,将有随请求一起传递的 jwt 令牌,您将提取并验证令牌并在安全上下文中设置身份验证。 SecurityContextHolder 中的Authentication 对象,它会自动认为它已通过身份验证? 在这种情况下,它可能会覆盖细节。
    【解决方案2】:
    1. 如果不知道这个 if 语句发生在哪里,很难评论它是否没有必要。如果请求不需要认证,认证可能为空,但也可能有其他情况。

      如果请求确实需要身份验证,那么一旦您的 servlet 被调用,身份验证不应为空。

    2. 线程不绑定到给定的用户会话。使用 Servlet,从线程池中为每个 HTTP 请求分配一个线程。

      SecurityContextHolder 为每个请求重新建立,方法是从会话中提取现有身份验证,或者在您的情况下,从请求数据中提取。

    【讨论】:

    • 嘿,非常感谢 O'Reilly 的春季安全课程。知识太丰富了。
    猜你喜欢
    • 2017-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-01
    • 1970-01-01
    • 2012-08-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多