【问题标题】:Reload UserDetails Object from Database Every Request in Spring SecuritySpring Security 中的每个请求都从数据库中重新加载 UserDetails 对象
【发布时间】:2014-05-29 03:06:46
【问题描述】:

我一直在寻找一种方法来重新加载每个请求的 Spring Security UserDetails 对象,但在任何地方都找不到示例。

有人知道怎么做吗?

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

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

感谢任何友好的建议。

【问题讨论】:

  • 您使用什么方法处理您的请求?只是 Spring MVC 或 REST,因为如果您使用的是 REST,您可以将您的方法保护到用户权限并使用实现 HandlerInterceptorAdapter 的类。如果您使用的是 REST,只需 google 保护 REST 控制器 Spring Framework,您应该会得到一些不错的示例
  • 你可以在这个帖子stackoverflow.com/questions/892733/…找到答案

标签: java spring spring-security


【解决方案1】:

最后,两年后,对于上面的问题和this question 之后的六年,这里是关于如何使用 Spring 为每个请求重新加载用户的 UserDetails 的答案...

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

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


一些尚未完全弄清楚的令人担忧的事情。

这段代码线程安全吗?

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

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


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

很有可能,但我们的网络服务器响应时间没有明显变化。

关于这个主题的一些简短说明......

  • 数据库非常智能,它们有算法可以知道缓存特定查询的内容和时间。
  • 我们正在使用 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>

【讨论】:

  • Question 提到了正确的answer。我已在评论中添加了错过的配置详细信息。
【解决方案2】:

对于管理员用户更改其他用户权限的一方:您可以尝试检索受影响用户的会话并设置一些属性以指示需要重新加载。

如果你碰巧使用了 Spring Session,并且会话属性保存在数据库中(例如,为了支持多个容器实例),你可以在管理员用户更改权限时标记一个活动会话以重新加载:

@Autowired
private FindByIndexNameSessionRepository<Session> sessionRepo;

public void tag(String username) {
    Map<String, Session> sessions = sessionRepo.findByIndexNameAndIndexValue
            (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
    for (Session s : sessions.values()) {
        s.setAttribute("reloadAuth", true);
        sessionRepo.save(s);
    }
}

对于已登录的用户端:您可以编写 Spring Security Filter 来检查会话属性是否重新加载当前会话的身份验证。如果管理员将其标记为重新加载,我们会再次从数据库中检索Principal 并重新设置我们的Authentication

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpSession session = ((HttpServletRequest) request).getSession();
    if (session != null) {
        Boolean reload = (Boolean) session.getAttribute("reloadAuth");
        if (Boolean.TRUE.equals(shouldReloadRoles)) {
            session.removeAttribute("reloadAuth");
            /* Do some locking based on session ID if you want just to avoid multiple reloads for a session */
            Authentication newAuth = ... // Load new authentication from DB
            SecurityContextHolder.getContext().setAuthentication(newAuth);
        }
    }
    chain.doFilter(request, response);
}

【讨论】:

    【解决方案3】:

    您好,所以想与此问题分享一些与基于令牌的身份验证相关的内容,在我的例子中是 Oauth2。起初我尝试了上面 hooknc 的方法,在我的例子中我使用的是基于 Token 的身份验证,所以我的身份验证对象是 instanceOf Oauth2Authentication。与标准 Authentication Principal 不同,Oauth2Authentication 对象由 Authorization Request 和 Authentication 对象构成。此外,主体是通过使用令牌本身构造的。因此,当尝试在另一个调用中重用令牌时,它最终会以旧的用户数据为主。所以这种方法不适用于基于令牌的身份验证。

    我要明确的原始问题是在用户更新用户设置之后,如果用户在之后进行其他 API 调用,则会导致旧的用户信息。与其尝试更新主体,我发现在更新后发布新令牌是一种更好的方法。

    我还应该补充一点,我的身份验证 Oauth2 方案是完全无状态的,所有内容都存储在数据库中。

    【讨论】:

    • 非常感谢您
    【解决方案4】:

    我正在尝试 FilterSecurityInterceptor 的重新认证技巧

    使用 JDBC userDetailsS​​ervice 进行表单登录

    1. 身份验证的 isAuthenticated 设置为返回 false。
    2. rase-credentials 为 false。 (在会话中保留凭据...
    3. 当 AuthenticationException 引发时,注销然后重定向到登录页面。

    身份验证提供者

    认证为假。

    package studying.spring;
    
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.UserDetails;
    
    public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
    
      @Override
      protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    
        Authentication result = super.createSuccessAuthentication(principal, authentication, user);
        result.setAuthenticated(false);
    
        return result;
      }
    }
    

    ExceptionTranslationFilter 的AuthenticationEntryPoint

    注销并重定向到登录页面。

    package studying.spring;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
    import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
    
    public class MyLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
    
      public MyLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
        super(loginFormUrl);
      }
    
      @Override
      protected String determineUrlToUseForThisRequest(
          HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
    
        if (exception != null) {
          Authentication auth = SecurityContextHolder.getContext().getAuthentication();
          new SecurityContextLogoutHandler().logout(request, response, auth);
          SecurityContextHolder.getContext().setAuthentication(null);
        }
    
        return super.determineUrlToUseForThisRequest(request, response, exception);
      }
    }
    

    root-context.xml

    rase-credentials 属性。为假。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:security="http://www.springframework.org/schema/security"
        xsi:schemaLocation="
          http://www.springframework.org/schema/security
          http://www.springframework.org/schema/security/spring-security-4.1.xsd
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
    
      <security:http entry-point-ref="myLoginUrlAuthenticationEntryPoint">
        <security:intercept-url pattern="/**" access="hasRole('USER')"/>
        <security:form-login/>
        <security:logout/>
      </security:http>
    
      <security:authentication-manager erase-credentials="false">
        <security:authentication-provider ref="myDaoAuthenticationProvider"/>
      </security:authentication-manager>
    
      <bean id="myLoginUrlAuthenticationEntryPoint" class="studying.spring.MyLoginUrlAuthenticationEntryPoint">
        <constructor-arg name="loginFormUrl" value="/login" />
      </bean>
    
      <bean id="myDaoAuthenticationProvider" class="studying.spring.MyDaoAuthenticationProvider">
        <property name="userDetailsService" ref="jdbcDaoImpl" />
      </bean>
    
      <bean id="jdbcDaoImpl" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
        <property name="dataSource" ref="securityDataSource" />
      </bean>
    
      <bean id="securityDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/studying_spring_security" />
        <property name="username" value="root" />
        <property name="password" value="" />
      </bean>
    </beans>
    

    【讨论】:

      猜你喜欢
      • 2020-01-21
      • 1970-01-01
      • 2019-10-20
      • 2011-05-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-22
      • 2014-12-14
      相关资源
      最近更新 更多