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

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

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

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

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

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

所以,最后经过两年的上述问题和六年后的问题 ,有一个答案是如何使用Spring重新加载每个请求的用户的UserDetails …

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

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

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

这段代码线程安全吗?

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

我们的代码“风险足够低”,我们还没有尝试过测试潜在的multithreading问题。

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

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

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

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

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

  • 它用来表示我们用来表示Principal的UserDetails对象不是Serializable,因此当我们停止并重新启动我们的tomcat服务器时,所有反序列化的SercurityContexts都会有一个null主体对象,我们的最终用户会收到服务器错误到期空指针exception。 现在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:

          

我正在尝试FilterSecurityInterceptor的重新validation技巧

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

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

的AuthenticationProvider

validation为false。

 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; } } 

AuthenticationEntryPoint for ExceptionTranslationFilter

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

 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); } } 

根的context.xml

rase-credentials attr。 为假。

                           

您好,我想在基于令牌的身份validation中分享与此问题相关的内容,在我的案例中是Oauth2。 起初我尝试了上面的hooknc的方法,在我的情况下我使用基于令牌的身份validation,所以我的身份validation对象是instanceOf Oauth2Authentication。 与标准身份validation主体不同,Oauth2Authentication对象由授权请求和身份validation对象构成。 此外,通过使用令牌本身构造主体。 因此,当尝试在另一个调用中重用该令牌时,它最终会以原始用户数据结束。 因此,此方法不适用于基于令牌的身份validation。

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

我还应该补充一点,我的Authentication Oauth2方案完全没有状态,一切都存储在DB中。

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

如果您碰巧使用Spring Session并在数据库中保留了会话属性(例如,为了支持多个容器实例),则可以在admin用户对权限进行更改时标记活动会话以进行重新加载:

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

对于登录用户端 :您可以编写Spring Security Filter来检查会话属性是否重新加载当前会话的身份validation。 如果管理员已将其标记为重新加载,我们将再次从数据库中检索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); }