Finally, after two years, for the question above and for six years after this question, here is an answer on how to reload a user's UserDetails per request with Spring...
To reload a user/security context per request, it is important to override the default behavior of Spring Security's HttpSessionSecurityContextRepository, which implements the SecurityContextRepository interface.
The HttpSessionSecurityContextRepository is the class that is used by Spring Security to get the user's security context from the HttpSession. The code that calls this class is what places the SecurityContext on threadlocal. So when the loadContext(HttpRequestResponseHolder requestResponseHolder)
method is called we can turn around and make a request to a DAO or Repository and reload the user/principal.
Some things of concern that have not quite been figured out quite yet.
Is this code thread safe?
I have no idea, it depends on if there is a new SecurityContext created per thread/request into the web server. If there is a new SecurityContext created life is good, but if not, there could be some interesting unexpected behavior like stale object exceptions, the wrong state for a user/principal being saved to the data store, etc...
Our code is 'low risk enough' that we haven't tried to test potential multi-thread issues.
Is there a performance hit for calling to the database every request?
Most likely, but we haven't seen noticeable change in our web server response times.
A couple of quick notes on this subject...
- Databases are super smart and they have algorithms to know what and when to cache specific queries.
- We are using hibernate's second level caching.
Benefits we've received from this change:
- It use to be that our UserDetails object that we used to represent the Principal was not Serializable and therefore when we stopped and restarted our tomcat server, all the de-serialized SercurityContexts would have a null principal object and our end users would receive server errors due to null pointer exceptions. Now that the UserDetails/Principal object is serializable and the user is reloaded per request we can start/restart our server without having to clean out the work directory.
- We have received zero customer complaints about their new permissions not taking affect right away.
The Code
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 {
private UserRepository userRepository;
public ReloadUserPerRequestHttpSessionSecurityContextRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
SecurityContext context = super.loadContext(requestResponseHolder);
Authentication authentication = context.getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UserDetails userDetails = this.createNewUserDetailsFromPrincipal(authentication.getPrincipal());
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;
OpenIDAuthenticationToken newAuthentication = new OpenIDAuthenticationToken(userDetails, userDetails.getAuthorities(), openidAuthenticationToken.getIdentityUrl(), openidAuthenticationToken.getAttributes());
context.setAuthentication(newAuthentication);
}
return context;
}
private UserDetails createNewUserDetailsFromPrincipal(Object principal) {
AcegiUserDetails userDetails = (AcegiUserDetails) principal;
User user = this.userRepository.getUserFromSecondaryCache(userDetails.getUserIdentifier());
userDetails = new AcegiUserDetails(user);
return userDetails;
}
}
To plug a new SecurityContextRepository with xml configuration, just set the security-context-repository-ref attribute on the security:http context.
Example 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" >
</security:http>
<bean id="securityContextRepository" class="xxx.security.impl.spring.ReloadUserPerRequestHttpSessionSecurityContextRepository" >
<constructor-arg index="0" ref="userRepository"/>
</bean>
</beans>