【问题标题】:Why is the Authentication object of the SecurityContext not shared across threads?为什么SecurityContext的Authentication对象没有跨线程共享?
【发布时间】:2016-03-20 07:55:58
【问题描述】:

我有时会收到AuthenticationCredentialsNotFoundException 的问题。目前我认为这是一个线程问题。根据另一个问题 (link),SecurityContext 在不同线程之间通过 HttpSession 对象传递 - 但由于某种原因,这对我不起作用。

这就是我目前处理登录的方式:

public ShopAdminDTO login(String userEmail, String password) throws EmailAddressNotFoundException {

    LOGGER.debug("Login request for " + userEmail);

    // Create and initialize user details object for Spring Security authentication mechanism.
    ShopAdminUserDetails userDetails = new ShopAdminUserDetails(userEmail, password, true, true, true, true, new ArrayList<GrantedAuthority>());

    // Create authentication object for the Spring SecurityContext
    Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, password, new ArrayList<GrantedAuthority>());

    boolean requiresEmailActivation = this.shopAdminValidationTokenRepository.getRequiresEmailValidation(userEmail);

    if(requiresEmailActivation == true) {

        LOGGER.info("Login denied: Email is not validated yet.");

        // IMPORTANT NOTE: We throw an EmailNotFoundException instead of a
        // PleaseValidateYourEmailFirstException in order to NOT reveal
        // that this email exists. So: Do not "FIX" this!
        throw new EmailAddressNotFoundException();
    }

    LOGGER.debug("Email appears validated.");

    try {
        // Execute authentication chain to try user authentication
        auth = this.adminAuthenticationProvider.authenticate(auth);
    } catch(BadCredentialsException e) {
        // FIXME Login: We could/should count and limit login attempts here?
        LOGGER.info("Bad credentials found for: " + userEmail);
        throw e;
    }

    LOGGER.info("User successfully authenticated [userEmail="+userEmail+"]");

    // Set the authentication to the SecurityContext, the user is now logged in
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(auth);


    // Finally load the user data
    ShopAdminDTO shopAdminDto = this.shopAdminRepository.findByUserEmail(userEmail);
    return shopAdminDto;
}

这是 applicationContext-security.xml 文件

<!-- //////////////////////////////////////////////////////////////////////////////// -->
<!-- // BEGIN Spring Security -->

<sec:http pattern="/**" auto-config="true" use-expressions="true"/>

<bean id="httpSessionSecurityContextRepository" class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
    <property name='allowSessionCreation' value='false' />
</bean>

<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
    <constructor-arg ref="httpSessionSecurityContextRepository" />
</bean>

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
    <constructor-arg>
        <list>
            <sec:filter-chain pattern="/**" filters="securityContextPersistenceFilter" />
        </list>
    </constructor-arg>
</bean>

<bean id="authenticationListener" class="com.mz.server.web.auth.CustomAuthenticationListener"/>

<bean id="authenticationProvider" class="com.mz.server.web.auth.CustomAuthenticationProvider"/>

<bean id="userDetailsService" class="com.mz.server.web.service.CustomUserDetailsService"/>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="authenticationProvider"/>
</sec:authentication-manager>

<bean id="permissionEvaluator"
      class="com.mz.server.web.auth.permission.CustomPermissionEvaluator">
    <constructor-arg index="0">
        <map key-type="java.lang.String"
             value-type="com.mz.server.web.auth.permission.Permission">
            <entry key="isTest" value-ref="testPermission"/>
        </map>
    </constructor-arg>
</bean>

<bean id="testPermission" class="com.mz.server.web.auth.permission.TestPermission">
</bean>

<bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
    <property name="permissionEvaluator" ref="permissionEvaluator"/>
</bean>

<sec:global-method-security 
    authentication-manager-ref="authenticationManager"
    pre-post-annotations="enabled">     
    <sec:expression-handler ref="expressionHandler"/>       
</sec:global-method-security>

<!-- // END Spring Security -->
<!-- //////////////////////////////////////////////////////////////////////////////// -->

失败的是AbstractSecurityInterceptor#beforeInvocation 函数的这一部分:

if (debug) {
    logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}

if (SecurityContextHolder.getContext().getAuthentication() == null) {
    credentialsNotFound(messages.getMessage(
            "AbstractSecurityInterceptor.authenticationNotFound",
            "An Authentication object was not found in the SecurityContext"),
            object, attributes);
}

Authentication authenticated = authenticateIfRequired();

它在哪里调用credentialsNotFound,因为SecurityContextHolder.getContext().getAuthentication()null


比较启动服务器后第一次登录失败的堆栈跟踪:

[http-bio-8080-exec-4] DEBUG com.mz.server.servlet.LoginServletImpl - Login request by userId: user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.service.LoginService - Login request for user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.service.LoginService - Email appears validated.. authenticating..
[http-bio-8080-exec-4] INFO  com.mz.server.spring.auth.AdminAuthenticationProvider - authenticate(), User email: user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - findByUserEmail(): user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - User found.
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - Loading password salt for user@gmx.at
[http-bio-8080-exec-4] INFO  com.mz.server.repository.jooq.shop.ShopAdminRepository - Checking password for user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - Password valid.
[http-bio-8080-exec-4] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - getPrincipal()
[http-bio-8080-exec-4] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - Setting user com.mz.server.spring.auth.ShopAdminUserDetails@8ac733b2: Username: user@gmx.at; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Not granted any authorities to 'authenticated'.
[http-bio-8080-exec-4] INFO  com.mz.server.service.LoginService - User successfully authenticated [userEmail=user@gmx.at]
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - findByUserEmail(): user@gmx.at
[http-bio-8080-exec-4] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - User found.
[http-bio-8080-exec-6] DEBUG com.mz.server.servlet.shop.ShopServletImpl - Requested available shops.
[http-bio-8080-exec-6] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-6] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-6] INFO  com.mz.server.servlet.shop.ShopServletImpl - SPRING_SECURITY_CONTEXT: org.springframework.security.core.context.SecurityContextImpl@259bee56: Authentication: com.mz.server.spring.auth.CustomUserAuthentication@259bee56
[http-bio-8080-exec-6] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-6] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-6] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List com.mz.server.service.ShopService.getAvailableShops(); target is of class [com.mz.server.service.ShopService]; Attributes: [[authorize: 'isAuthenticated()', filter: 'null', filterTarget: 'null']]
[http-bio-8080-exec-6] DEBUG com.mz.server.spring.auth.CustomHttpSessionListener - AuthenticationCredentialsNotFoundEvent
Jun 09, 2016 8:06:42 PM org.apache.catalina.core.ApplicationContext log
SEVERE: Exception while dispatching incoming RPC call
com.google.gwt.user.server.rpc.UnexpectedException: Service method 'public abstract java.util.List com.mz.shared.web.service.shop.ShopServlet.getAvailableShops()' threw an unexpected exception: org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    at com.google.gwt.user.server.rpc.RPC.encodeResponseForFailure(RPC.java:416)
    at com.google.gwt.user.server.rpc.RPC.invokeAndEncodeResponse(RPC.java:605)
....

到工作堆栈跟踪:

[http-bio-8080-exec-7] DEBUG com.mz.server.servlet.LoginServletImpl - Login request by userId: user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.service.LoginService - Login request for user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.service.LoginService - Email appears validated.. authenticating..
[http-bio-8080-exec-7] INFO  com.mz.server.spring.auth.AdminAuthenticationProvider - authenticate(), User email: user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - findByUserEmail(): user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - User found.
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - Loading password salt for user@gmx.at
[http-bio-8080-exec-7] INFO  com.mz.server.repository.jooq.shop.ShopAdminRepository - Checking password for user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - Password valid.
[http-bio-8080-exec-7] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - getPrincipal()
[http-bio-8080-exec-7] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - Setting user com.mz.server.spring.auth.ShopAdminUserDetails@8ac733b2: Username: user@gmx.at; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Not granted any authorities to 'authenticated'.
[http-bio-8080-exec-7] INFO  com.mz.server.service.LoginService - User successfully authenticated [userEmail=user@gmx.at]
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - findByUserEmail(): user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - User found.
[http-bio-8080-exec-7] DEBUG com.mz.server.servlet.shop.ShopServletImpl - Requested available shops.
[http-bio-8080-exec-7] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-7] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-7] INFO  com.mz.server.servlet.shop.ShopServletImpl - SPRING_SECURITY_CONTEXT: org.springframework.security.core.context.SecurityContextImpl@1ea22883: Authentication: com.mz.server.spring.auth.CustomUserAuthentication@1ea22883
[http-bio-8080-exec-7] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-7] INFO  com.mz.server.servlet.shop.ShopServletImpl - 
[http-bio-8080-exec-7] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List com.mz.server.service.ShopService.getAvailableShops(); target is of class [com.mz.server.service.ShopService]; Attributes: [[authorize: 'isAuthenticated()', filter: 'null', filterTarget: 'null']]
[http-bio-8080-exec-7] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - isAuthenticate(): true
[http-bio-8080-exec-7] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Previously Authenticated: com.mz.server.spring.auth.CustomUserAuthentication@1ea22883
[http-bio-8080-exec-7] DEBUG org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@653fccd, returned: 1
[http-bio-8080-exec-7] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Authorization successful
[http-bio-8080-exec-7] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - RunAsManager did not change Authentication object
[http-bio-8080-exec-7] DEBUG com.mz.server.service.ShopService - Getting available shops for ..
[http-bio-8080-exec-7] DEBUG com.mz.server.spring.auth.CustomUserAuthentication - getPrincipal()
[http-bio-8080-exec-7] DEBUG com.mz.server.service.ShopService - user@gmx.at
[http-bio-8080-exec-7] DEBUG com.mz.server.repository.jooq.shop.ShopAdminRepository - Fetching shops for shop_admin_id 1

我们可以看到不同之处在于第一个堆栈跟踪是由两个线程[http-bio-8080-exec-4][http-bio-8080-exec-6]产生的。我经常看到这种行为,即线程名称发生更改,然后出现此异常。所以这似乎是一个多线程问题


这是整个web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <display-name>mz | life</display-name>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/**</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>

    <listener>
        <listener-class>com.mz.server.BootstrappingServerConfig</listener-class>
    </listener>

    <!-- -->

    <servlet>
        <servlet-name>application</servlet-name>
        <servlet-class>com.mz.server.servlet.app.ApplicationDataServletImpl</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>application</servlet-name>
        <url-pattern>/app/application</url-pattern>
    </servlet-mapping>

    <!-- -->

    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>com.mz.server.servlet.LoginServletImpl</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/app/login</url-pattern>
    </servlet-mapping>

    <!-- -->

    <servlet>
        <servlet-name>shop</servlet-name>
        <servlet-class>com.mz.server.servlet.shop.ShopServletImpl</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>shop</servlet-name>
        <url-pattern>/app/shop</url-pattern>
    </servlet-mapping>

    <!-- -->

    <servlet>
        <servlet-name>shopadmin</servlet-name>
        <servlet-class>com.mz.server.servlet.shop.ShopAdminServletImpl</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>shopadmin</servlet-name>
        <url-pattern>/app/shopadmin</url-pattern>
    </servlet-mapping>

    <!-- 
        XSRF-Token Servlet 
    -->

    <servlet>
        <servlet-name>xsrf</servlet-name>
        <servlet-class>com.google.gwt.user.server.rpc.XsrfTokenServiceServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>xsrf</servlet-name>
        <url-pattern>/app/xsrf</url-pattern>
    </servlet-mapping>

    <!-- 
        This is the name of the session cookie set by the Servlet container (e.g. Tomcat or Jetty) 
    -->
    <context-param>
        <param-name>gwt.xsrf.session_cookie_name</param-name>
        <param-value>JSESSIONID</param-value>
    </context-param>

    <!-- -->

    <servlet>
        <servlet-name>mobile-restapi</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mobile-restapi</servlet-name>
        <url-pattern>/app/restapi/*</url-pattern>
    </servlet-mapping>

    <!-- -->

    <servlet>
        <servlet-name>web-restapi</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/classes/context/applicationContext-restapi.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>web-restapi</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/classes/context/root-context.xml
        </param-value>
    </context-param>


</web-app>

【问题讨论】:

  • 您知道,不同的请求首先尝试访问哪些资源?您如何提供静态内容?是否有可能其中一个线程尝试从登录页面访问某些受保护的资源(图像/css)?
  • @StefanHaberl 好吧,您看到的日志来自于单击登录按钮的请求。这实际上并没有做更多的事情,只是查看数据库并检查给定用户名的密码是否正确。请参阅User successfully authenticated [userEmail=user@gmx.at] 条目,它告诉我登录实际上正在工作。所以不,此操作不涉及其他资源。

标签: spring multithreading spring-security


【解决方案1】:

您可以将 SecurityContext 从一个线程转移到另一个

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};
SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

查看并发支持

https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/concurrency.html

【讨论】:

    【解决方案2】:

    好吧,我有这个:

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/**</url-pattern>
    </filter-mapping>
    

    并简单地将其更改为:

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    

    我也删了

    <sec:http pattern="/**" auto-config="true" use-expressions="true" />
    

    在我的 applicationContext-spring.xml 中,我添加了这个别名

    <alias name="filterChainProxy" alias="springSecurityFilterChain"/>
    

    最终的applicationContext-spring.xml长这样:

    <beans xmlns="http://www.springframework.org/schema/beans"
    
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    
        xmlns:sec="http://www.springframework.org/schema/security"
    
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/security 
        http://www.springframework.org/schema/security/spring-security-4.0.xsd"    
        >
    
        <!-- Imports -->
        <import resource="applicationContext-spring-acl.xml"/>
    
        <bean id="authenticationListener" class="com.mahlzeit.server.spring.auth.CustomAuthenticationListener"/>
    
        <bean id="httpSessionListener" class="com.mahlzeit.server.spring.auth.CustomHttpSessionListener"/>
    
        <bean id="adminAuthenticationProvider" class="com.mahlzeit.server.spring.auth.AdminAuthenticationProvider">
            <constructor-arg ref="dslContext" />
        </bean>
    
        <bean id="userDetailsService" class="com.mahlzeit.server.service.CustomUserDetailsService"/>
    
        <sec:authentication-manager alias="authenticationManager">
            <sec:authentication-provider ref="adminAuthenticationProvider"/>
        </sec:authentication-manager>
    
        <!-- Filter Chain -->
    
        <bean id="httpSessionSecurityContextRepository" class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'/>
    
        <bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
            <constructor-arg ref="httpSessionSecurityContextRepository" />
        </bean>
    
        <alias name="filterChainProxy" alias="springSecurityFilterChain"/>
    
        <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
            <constructor-arg>
                <list>
                    <sec:filter-chain pattern="/**" filters="securityContextPersistenceFilter" />
                </list>
            </constructor-arg>
        </bean>
    
    </beans>
    

    我以this 作为参考。

    【讨论】:

      【解决方案3】:

      您已将allowSessionCreation 设置为false

      请注意,将此标志设置为 false 不会阻止此类存储安全上下文。如果您的应用程序(或其他过滤器)创建了一个会话,那么安全上下文仍会为经过身份验证的用户存储。

      您是否正确地创建了会话?或者您真的希望安全性不要创建会话?

      【讨论】:

      • 嗨!哇,有趣 :D 我想我是很久以前从教程中得到的。我认为我想我希望 Spring Security 创建会话。我想登录一个用户并让它在会话期间随意发送请求。为什么有人不想开会?我不确定我自己是否正确创建了会话,但我会尝试,但我会在回家后立即将 allowSessionCreation 设置为 true,并让您知道这是否有效。
      • 我尝试将其设置为true,但还是一样。当一个不同的线程进来时,我得到了异常。
      • 你能发布任何其他相关的配置吗?例如,web.xml
      • 你是如何启动这个应用程序的?
      • 我已将 web.xml 添加到我的问题中。 “如何”是什么意思?它在 Servlet 容器中运行 - Tomcat/Jetty。
      猜你喜欢
      • 1970-01-01
      • 2018-06-23
      • 2013-05-09
      • 2010-11-29
      • 2013-05-18
      • 2016-03-11
      • 2016-11-12
      • 2012-12-13
      • 1970-01-01
      相关资源
      最近更新 更多