从Spring Database中的数据库每个请求重新加载UserDetails对象

hoo*_*knc 7 java spring spring-security

我一直在寻找一种方法来为每个请求重新加载Spring Security UserDetails对象,并且无法在任何地方找到示例.

有谁知道怎么做这样的事情?

基本上,我们希望每个请求都重新加载用户权限,因为该用户的权限可能会从Web请求更改为Web请求.

例如,登录并随后被授予新权限的用户(并通知他们通过电子邮件拥有新权限),我知道该用户实际获得该新权限的唯一方法是通过注销然后再次登录.我想尽可能避免.

任何友好的建议表示赞赏.

hoo*_*knc 7

最后,两年后,针对上述问题以及此问题后的六年,这里是一个关于如何使用Spring重新加载每个请求的用户UserDetails的答案...

要为每个请求重新加载用户/安全上下文,重要的是覆盖Spring Security的HttpSessionSecurityContextRepository的默认行为,该行为实现SecurityContextRepository接口.

HttpSessionSecurityContextRepository是Spring Security用于从HttpSession获取用户安全上下文的类.调用此类的代码是将SecurityContext放在threadlocal上的代码.因此,当loadContext(HttpRequestResponseHolder requestResponseHolder)调用该方法时,我们可以转向并向DAORepository发出请求并重新加载用户/主体.


一些尚未完全弄清楚的关注事项.

这段代码线程安全吗?

我不知道,这取决于每个线程/请求是否在Web服务器中创建了新的SecurityContext.如果有一个新的SecurityContext创建生活是好的,但如果没有,可能会有一些有趣的意外行为,如陈旧的对象异常,用户/主体被保存到数据存储的错误状态等...

我们的代码"风险足够低",我们还没有尝试过测试潜在的多线程问题.


每次请求调用数据库是否会影响性能?

最有可能,但我们的Web服务器响应时间没有发生明显变化.

关于这个主题的几个快速笔记......

  • 数据库非常智能,他们有算法知道缓存特定查询的内容和时间.
  • 我们正在使用hibernate的二级缓存.


我们从这一变化中获得的好处:

  • 它用来表示我们用来表示Principal的UserDetails对象不是Serializable,因此当我们停止并重新启动我们的tomcat服务器时,所有反序列化的SercurityContexts将有一个空主体对象,我们的最终用户将收到服务器错误到期空指针异常.现在UserDetails/Principal对象是可序列化的,并且每个请求重新加载用户,我们可以启动/重新启动我们的服务器而无需清理工作目录.
  • 我们收到零客户投诉,说明他们的新权限不会立即生效.


代码

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.openid.OpenIDAuthenticationToken;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import xxx.repository.security.UserRepository;
import xxx.model.security.User;
import xxx.service.security.impl.acegi.AcegiUserDetails;

public class ReloadUserPerRequestHttpSessionSecurityContextRepository extends HttpSessionSecurityContextRepository {

    // Your particular data store object would be used here...
    private UserRepository userRepository;

    public ReloadUserPerRequestHttpSessionSecurityContextRepository(UserRepository userRepository) {

        this.userRepository = userRepository;
    }

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {

        // Let the parent class actually get the SecurityContext from the HTTPSession first.
        SecurityContext context = super.loadContext(requestResponseHolder);

        Authentication authentication = context.getAuthentication();

        // We have two types of logins for our system, username/password
        // and Openid, you will have to specialize this code for your particular application.
        if (authentication instanceof UsernamePasswordAuthenticationToken) {

            UserDetails userDetails = this.createNewUserDetailsFromPrincipal(authentication.getPrincipal());

            // Create a new Authentication object, Authentications are immutable.
            UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());

            context.setAuthentication(newAuthentication);

        } else if (authentication instanceof OpenIDAuthenticationToken) {

            UserDetails userDetails = this.createNewUserDetailsFromPrincipal(authentication.getPrincipal());

            OpenIDAuthenticationToken openidAuthenticationToken = (OpenIDAuthenticationToken) authentication;

            // Create a new Authentication object, Authentications are immutable.
            OpenIDAuthenticationToken newAuthentication = new OpenIDAuthenticationToken(userDetails, userDetails.getAuthorities(), openidAuthenticationToken.getIdentityUrl(), openidAuthenticationToken.getAttributes());

            context.setAuthentication(newAuthentication);
        }

        return context;
    }

    private UserDetails createNewUserDetailsFromPrincipal(Object principal) {

        // This is the class we use to implement the Spring Security UserDetails interface.
        AcegiUserDetails userDetails = (AcegiUserDetails) principal;

        User user = this.userRepository.getUserFromSecondaryCache(userDetails.getUserIdentifier());

        // NOTE:  We create a new UserDetails by passing in our non-serializable object 'User', but that object in the AcegiUserDetails is transient.
        // We use a UUID (which is serializable) to reload the user.  See the userDetails.getUserIdentifier() method above.
        userDetails = new AcegiUserDetails(user);

        return userDetails;
    }
}
Run Code Online (Sandbox Code Playgroud)


要使用xml配置插入新的SecurityContextRepository,只需在security:http上下文中设置security-context-repository-ref属性.

示例xml:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                           http://www.springframework.org/schema/security
                           http://www.springframework.org/schema/security/spring-security-4.0.xsd">
    <security:http context-repository-ref="securityContextRepository" >
         <!-- intercept-url and other security configuration here... -->
    </security:http>

    <bean id="securityContextRepository" class="xxx.security.impl.spring.ReloadUserPerRequestHttpSessionSecurityContextRepository" >
        <constructor-arg index="0" ref="userRepository"/>
    </bean>
</beans>
Run Code Online (Sandbox Code Playgroud)