当前位置: 首页 > 面试题库 >

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

翟京
2023-03-14
问题内容

我一直在寻找一种方法,可以在每个请求时重新加载我们的Spring Security UserDetails对象,并且无法在任何地方找到示例。

有人知道该怎么做吗?

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

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

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


问题答案:

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

为了按请求重新加载用户/安全上下文,重要的是重写Spring
Security的HttpSessionSecurityContextRepository的默认行为,该行为实现了SecurityContextRepository接口。

HttpSessionSecurityContextRepository是Spring
Security用来从HttpSession获取用户的安全上下文的类。调用此类的代码是将SecurityContext置于threadlocal上的原因。因此,在loadContext(HttpRequestResponseHolder requestResponseHolder)调用该方法时,我们可以转过来向DAO或存储库发出请求,然后重新加载用户/主要用户。

尚未完全解决一些令人关注的问题。

此代码线程安全吗?

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

我们的代码“足够低风险”,因此我们没有尝试测试潜在的多线程问题。

每次请求调用数据库都会对性能产生影响吗?

很有可能,但我们尚未看到Web服务器响应时间的明显变化。

关于此主题的几点快速注释…

  • 数据库是超级智能的,它们具有知道什么以及何时缓存特定查询的算法。
  • 我们正在使用hibernate的二级缓存。

我们从这项变更中获得的好处:

  • 过去我们用来表示Principal的UserDetails对象不是可序列化的,因此,当我们停止并重新启动tomcat服务器时,所有反序列化的SercurityContexts都将具有空的Principal对象,并且最终用户将收到服务器错误,原因是空指针异常。现在,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;
    }
}

要使用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>


 类似资料:
  • 基本上,我们希望每一个请求都重新加载用户的权限,因为用户的权限可能会在不同的web请求之间发生变化。 例如,一个用户登录并随后被授予新的权限(并通过电子邮件通知他们拥有新的权限),我知道该用户实际获得新权限的唯一方法是注销,然后再次登录。如果可能的话,我会尽量避免。 欢迎任何友好的建议。

  • 我不明白在spring mvc中使用dispatcher servlet只创建了一个bean对象,还是每个请求都创建了一个新对象? 控制器代码:- 在代码中,我在LoginBean对象中设置数据,并在方法abc中的modelandview对象中设置它。 然后,在jsp中,我没有为usename输入任何值,在这种情况下,当我提交表单并调用处理程序方法(initform)时,我试图打印相同的lb.ge

  • 我通过AJAX请求从数据库中获取表数据。我需要更改AJAX请求中的数据参数并刷新表。 我正在用命令刷新表格 我有以下代码 但是在AJAX重新加载之后,会向服务器发送原始请求,并忽略新的参数值。我试图通过函数、全局变量和浏览器存储将数据传递给请求,但这些方法都不起作用。在互联网上,我找到了解决问题的方法 功能,但我不知道如何使用它。 我的jQuery数据表版本是1.10.7。 我还尝试使用以下代码销

  • 问题内容: 我有ng-table的页面,可以很好地处理初始数据。我有一个选择框,该框根据选择的选项发送服务器请求,然后Iam将新数据获取为angular,但未使用ng- table更新。 这是我的看法: 我的控制器: 问题答案: $scope.$watch(‘result’, function () { $scope.tableParams.settings().$scope = $scope;

  • 问题内容: 每个HTTP请求是否在不同的线程中访问相同的servlet对象?还是创建一个新的线程和新的Servlet实例? 问题答案: 如果您的servlet未实现,则容器将使用相同的servlet实例。否则,不能保证击中相同的物体。如果认为必要,容器可以自由创建更多的servlet实例。但是请求来自不同的线程,不一定是新创建的(如Sanjay所述)。 根据Servlet 3.0规范: 对于未在分

  • 问题内容: 是否有一种惯用的方式让Flask在每次请求时重新加载我的配置文件?这样做的目的是使我可以更改密码或其他与配置有关的项目,而不必在生产环境中关闭并重新启动服务器。 编辑:是不可接受的,因为它会重新启动服务器,并且不应在生产中使用。 也许是这样的装饰器: 如果相关,这是我现在如何加载配置的方法: 我的档案: 我的档案: 问题答案: 在应用程序开始处理请求之后,您将无法安全/正确地重新加载配