Java 在Spring Security中,每个请求都从数据库重新加载UserDetails对象
我一直在寻找一种方法,在每个请求中重新加载我们的SpringSecurityUserDetails对象,但在任何地方都找不到示例 有人知道怎么做这样的事吗 基本上,我们希望在每个请求中重新加载用户的权限,因为该用户的权限可能会从一个web请求更改为另一个web请求 例如,一个用户登录并随后被授予新权限(并通过电子邮件通知他们拥有新权限),我知道该用户实际获得新权限的唯一方法是注销,然后再次登录。如果可能的话,我想避免Java 在Spring Security中,每个请求都从数据库重新加载UserDetails对象,java,spring,spring-security,Java,Spring,Spring Security,我一直在寻找一种方法,在每个请求中重新加载我们的SpringSecurityUserDetails对象,但在任何地方都找不到示例 有人知道怎么做这样的事吗 基本上,我们希望在每个请求中重新加载用户的权限,因为该用户的权限可能会从一个web请求更改为另一个web请求 例如,一个用户登录并随后被授予新权限(并通过电子邮件通知他们拥有新权限),我知道该用户实际获得新权限的唯一方法是注销,然后再次登录。如果可能的话,我想避免 非常感谢任何友好的建议。最后,两年后,对于上述问题以及六年后的问题,下面是关于
非常感谢任何友好的建议。最后,两年后,对于上述问题以及六年后的问题,下面是关于如何使用Spring重新加载每个请求的用户详细信息的答案 要为每个请求重新加载用户/安全上下文,必须重写Spring security的默认行为,它实现了接口 HttpSessionSecurityContextRepository是Spring Security用来从HttpSession获取用户安全上下文的类。调用此类的代码将SecurityContext放置在threadlocal上。因此,当调用
loadContext(HttpRequestResponseHolder requestResponseHolder)
方法时,我们可以掉头向or发出请求并重新加载用户/主体
有些问题还没有完全解决 此代码是线程安全的吗? 我不知道,这取决于是否有一个新的SecurityContext为web服务器中的每个线程/请求创建。如果创建了一个新的SecurityContext,则生命是美好的,但如果没有,则可能会出现一些有趣的意外行为,如过时的对象异常、将用户/主体保存到数据存储的错误状态等 我们的代码“风险足够低”,因此我们没有尝试测试潜在的多线程问题
每次请求调用数据库是否都会影响性能? 很有可能,但我们还没有看到web服务器响应时间的显著变化 关于这个问题的几个简短说明
- 数据库是超级智能的,它们有算法来知道缓存特定查询的内容和时间
- 我们正在使用hibernate的二级缓存
我们从这一变化中获得的好处:
- 过去,我们用来表示主体的UserDetails对象是不可序列化的,因此,当我们停止并重新启动tomcat服务器时,所有反序列化的SercurityContext都会有一个null主体对象,最终用户会因为null指针异常而收到服务器错误。既然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>
我正在尝试FilterSecurity Interceptor的重新验证技巧 用于使用JDBC userDetailsService进行表单登录
package.spring;
导入org.springframework.security.authentication.dao.DaoAuthenticationProvider;
导入org.springframework.security.core.Authentication;
导入org.springframework.security.core.userdetails.userdetails;
公共类MyDaoAuthenticationProvider扩展了DaoAuthenticationProvider{
@凌驾
受保护的身份验证CreateSuccesAuthentication(对象主体、身份验证、用户详细信息用户){
身份验证结果=super.createSuccessAuthentication(主体、身份验证、用户);
结果:setAuthenticated(false);
返回结果;
}
}
例外TranslationFilter的AuthenticationEntryPoint
注销并重定向到登录页面
package.spring;
导入javax.servlet.http.HttpServletRequest;
导入javax.servlet.http.HttpServletResponse;
导入org.springframework.security.core.Authentication;
导入org.springframework.security.core.AuthenticationException;
导入org.springframework.security.core.context.SecurityContextHolder;
导入org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
导入org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
公共类MyLoginRuthenAuthenticationEntryPoint扩展了LoginRuthenAuthenticationEntryPoint{
公共MyLoginUrlAuthenticationEntryPoint(字符串loginFormUrl){
超级(loginFormUrl);
}
@凌驾
此请求的受保护字符串determinuteUrlToUse(
HttpServletRequest请求、HttpServletResponse响应、AuthenticationException异常){
if(异常!=null){
Authentication auth=SecurityContextHolder.getContext().getAuthentication();
新的SecurityContextLogoutHandler()。注销(请求、响应、身份验证);
SecurityContextHolder.getContext().setAuthentication(null);
}
返回此请求的super.determineurltouse(请求、响应、异常);
}
}
根上下文.xml
将凭据属性设置为false
@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);
}
}
@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);
}