How to customize a spring-security form-login. Part 3
An article from my zk integration series.
AbstractUserDetailsAuthenticationProvider.java
The method additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) allow us additional checks for a returned UserDetails. We adapt it only to our UsernamePasswordTenantAuthenticationToken.
AbstractUserDetailsAuthenticationProvider.java
public abstract class MyAbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
// ~ Instance fields
// ================================================================================================
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
// ~ Methods
// ========================================================================================================
/**
* Allows subclasses to perform any additional checks of a returned (or
* cached) <code>UserDetails</code> for a given authentication request.
* Generally a subclass will at least compare the
* {@link Authentication#getCredentials()} with a
* {@link UserDetails#getPassword()}. If custom logic is needed to compare
* additional properties of <code>UserDetails</code> and/or
* <code>UsernamePasswordAuthenticationToken</code>, these should also
* appear in this method.
*
* @param userDetails
* as retrieved from the
* {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)}
* or <code>UserCache</code>
* @param authentication
* the current request that needs to be authenticated
*
* @throws AuthenticationException
* AuthenticationException if the credentials could not be
* validated (generally a <code>BadCredentialsException</code>,
* an <code>AuthenticationServiceException</code>)
*/
protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException;
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
doAfterPropertiesSet();
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordTenantAuthenticationToken.class, authentication, messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordTenantAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordTenantAuthenticationToken) authentication);
} catch (UsernameNotFoundException notFound) {
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
throw notFound;
}
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordTenantAuthenticationToken) authentication);
} catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordTenantAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordTenantAuthenticationToken) authentication);
} else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
/**
* Creates a successful {@link Authentication} object.
* <p>
* Protected so subclasses can override.
* </p>
* <p>
* Subclasses will usually store the original credentials the user supplied
* (not salted or encoded passwords) in the returned
* <code>Authentication</code> object.
* </p>
*
* @param principal
* that should be the principal in the returned object (defined
* by the {@link #isForcePrincipalAsString()} method)
* @param authentication
* that was presented to the provider for validation
* @param user
* that was loaded by the implementation
*
* @return the successful authentication token
*/
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
// OLD
// UsernamePasswordTenantAuthenticationToken result = new
// UsernamePasswordTenantAuthenticationToken(principal,
// authentication.getCredentials(), user.getAuthorities());
UsernamePasswordTenantAuthenticationToken result = new UsernamePasswordTenantAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities(),
((UsernamePasswordTenantAuthenticationToken) authentication).getTenantId());
result.setDetails(authentication.getDetails());
return result;
}
protected void doAfterPropertiesSet() throws Exception {
}
public UserCache getUserCache() {
return userCache;
}
public boolean isForcePrincipalAsString() {
return forcePrincipalAsString;
}
public boolean isHideUserNotFoundExceptions() {
return hideUserNotFoundExceptions;
}
/**
* Allows subclasses to actually retrieve the <code>UserDetails</code> from
* an implementation-specific location, with the option of throwing an
* <code>AuthenticationException</code> immediately if the presented
* credentials are incorrect (this is especially useful if it is necessary
* to bind to a resource as the user in order to obtain or generate a
* <code>UserDetails</code>).
* <p>
* Subclasses are not required to perform any caching, as the
* <code>AbstractUserDetailsAuthenticationProvider</code> will by default
* cache the <code>UserDetails</code>. The caching of
* <code>UserDetails</code> does present additional complexity as this means
* subsequent requests that rely on the cache will need to still have their
* credentials validated, even if the correctness of credentials was assured
* by subclasses adopting a binding-based strategy in this method.
* Accordingly it is important that subclasses either disable caching (if
* they want to ensure that this method is the only method that is capable
* of authenticating a request, as no <code>UserDetails</code> will ever be
* cached) or ensure subclasses implement
* {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
* to compare the credentials of a cached <code>UserDetails</code> with
* subsequent authentication requests.
* </p>
* <p>
* Most of the time subclasses will not perform credentials inspection in
* this method, instead performing it in
* {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
* so that code related to credentials validation need not be duplicated
* across two methods.
* </p>
*
* @param username
* The username to retrieve
* @param authentication
* The authentication request, which subclasses <em>may</em> need
* to perform a binding-based retrieval of the
* <code>UserDetails</code>
*
* @return the user information (never <code>null</code> - instead an
* exception should the thrown)
*
* @throws AuthenticationException
* if the credentials could not be validated (generally a
* <code>BadCredentialsException</code>, an
* <code>AuthenticationServiceException</code> or
* <code>UsernameNotFoundException</code>)
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException;
public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
this.forcePrincipalAsString = forcePrincipalAsString;
}
/**
* By default the <code>AbstractUserDetailsAuthenticationProvider</code>
* throws a <code>BadCredentialsException</code> if a username is not found
* or the password is incorrect. Setting this property to <code>false</code>
* will cause <code>UsernameNotFoundException</code>s to be thrown instead
* for the former. Note this is considered less secure than throwing
* <code>BadCredentialsException</code> for both exceptions.
*
* @param hideUserNotFoundExceptions
* set to <code>false</code> if you wish
* <code>UsernameNotFoundException</code>s to be thrown instead
* of the non-specific <code>BadCredentialsException</code>
* (defaults to <code>true</code>)
*/
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public boolean supports(Class<? extends Object> authentication) {
return (UsernamePasswordTenantAuthenticationToken.class.isAssignableFrom(authentication));
}
protected UserDetailsChecker getPreAuthenticationChecks() {
return preAuthenticationChecks;
}
/**
* Sets the policy will be used to verify the status of the loaded
* <tt>UserDetails</tt> <em>before</em> validation of the credentials takes
* place.
*
* @param preAuthenticationChecks
* strategy to be invoked prior to authentication.
*/
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
this.preAuthenticationChecks = preAuthenticationChecks;
}
protected UserDetailsChecker getPostAuthenticationChecks() {
return postAuthenticationChecks;
}
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
this.postAuthenticationChecks = postAuthenticationChecks;
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), user);
}
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"), user);
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), user);
}
}
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"), user);
}
}
}
}
MyDaoAuthenticationProvider.java
An implementation of an AuthenticationProvider that retrieves the user details from an UserDetailsService.
MyDaoAuthenticationProvider .java
public class MyDaoAuthenticationProvider extends MyAbstractUserDetailsAuthenticationProvider {
// ~ Instance fields
// ================================================================================================
private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
private SaltSource saltSource;
private UserDetailsService userDetailsService;
private boolean includeDetailsObject = true;
// ~ Methods
// ========================================================================================================
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), includeDetailsObject ? userDetails : null);
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), includeDetailsObject ? userDetails : null);
}
}
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
protected final UserDetails retrieveUser(String username, UsernamePasswordTenantAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username + "/" + authentication.getTenantId());
} catch (DataAccessException repositoryProblem) {
throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new AuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate
* passwords. If not set, {@link PlaintextPasswordEncoder} will be used by
* default.
*
* @param passwordEncoder
* The passwordEncoder to use
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
/**
* The source of salts to use when decoding passwords. <code>null</code> is
* a valid value, meaning the <code>DaoAuthenticationProvider</code> will
* present <code>null</code> to the relevant <code>PasswordEncoder</code>.
*
* @param saltSource
* to use when attempting to decode passwords via the
* <code>PasswordEncoder</code>
*/
public void setSaltSource(SaltSource saltSource) {
this.saltSource = saltSource;
}
protected SaltSource getSaltSource() {
return saltSource;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
protected boolean isIncludeDetailsObject() {
return includeDetailsObject;
}
/**
* Determines whether the UserDetails will be included in the
* <tt>extraInformation</tt> field of a thrown BadCredentialsException.
* Defaults to true, but can be set to false if the exception will be used
* with a remoting protocol, for example.
*
* @deprecated use
* {@link org.springframework.security.authentication.ProviderManager#setClearExtraInformation(boolean)}
*/
public void setIncludeDetailsObject(boolean includeDetailsObject) {
this.includeDetailsObject = includeDetailsObject;
}
}
Part 4 of this article you can read here.
Samples are hostet in the Zksample2 project on
Have fun with it.
Stephan Gerth
Dipl.rer.pol.
PS: Help to prevent the global warming by writing cool software