Spring Security authentication process

Spring Security authentication process

<Core filter loading> This article has been analyzed,

FilterChainProxy
After obtaining the filter chain, start to follow the logic of the filter chain. As shown in the figure,

these filter chains include the following

<Core Filter Introduction> section, which talks about the authentication logic of the form authentication method.
UsernamePasswordAuthenticationFilter
Proceeded, so let's start to analyze
UsernamePasswordAuthenticationFilter
, Before analyzing it, let s analyze its parent class first.

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { //~ Static fields/initializers //============================== ================================================== ===== //~ Instance fields //========================================= ================================================== === protected ApplicationEventPublisher eventPublisher; protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationManager authenticationManager; protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private RememberMeServices rememberMeServices = new NullRememberMeServices(); private RequestMatcher requiresAuthenticationRequestMatcher; private boolean continueChainBeforeSuccessfulAuthentication = false ; private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy(); private boolean allowSessionCreation = true ; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); //Filter authentication entrance public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //Whether the filter should verify the login request if (!requiresAuthentication(request, response)) { //If not, skip chain.doFilter(request, response); return ; } Authentication authResult; try { //Call the subclass to get the Authentication object (information of successful authentication, such as username, password, etc.) authResult = attemptAuthentication(request, response); if (authResult == null ) { //empty means return ; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user." , failed); //handle exception unsuccessfulAuthentication(request, response, failed); return ; } catch (AuthenticationException failed) { ////handle exception unsuccessfulAuthentication(request, response, failed); return ; } if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //Authentication success logic, such as saving context information and jumping to the login success page successfulAuthentication(request, response, chain, authResult); } protected boolean requiresAuthentication (HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); } //Subclass authentication logic public abstract Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException ; } Copy code

We can see the main work of the parent class from the source code

  • See if the current filter intercepts the login request
  • Invoke the authentication logic of the subclass
  • Perform login success or login failure processing according to the authentication result of the subclass


Let's take a look at the authentication logic of the subclass

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //~ Static fields/initializers //================================== ================================================== //User name and password receiving parameters have been predefined public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username" ; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password" ; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true ; //~ Constructors //=========================================== ================================================== ===== public UsernamePasswordAuthenticationFilter () { //Login authentication path super ( new AntPathRequestMatcher( "/login" , "POST" )); } //~ Methods //===================================================================================================================================================================================================================== ================================================== ========== public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals( "POST" )) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null ) { username = "" ; } if (password == null ) { password = "" ; } username = username.trim(); //Encapsulate it as UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //Allow subclasses to set the "details" property setDetails(request, authRequest); //Get the authentication manager and perform authentication return this .getAuthenticationManager().authenticate(authRequest); } } Copy code

Therefore, the sub-categories mainly do:

  • Get the username, password, and encapsulate it as
    UsernamePasswordAuthenticationToken
  • Obtain the authentication manager and pass the token into it for authentication


Let s take a look at how the authentication manager works, but before that, let s take a look

Authentication
,and also
UsernamePasswordAuthenticationToken
It's something (PS: <SpringSecurity core concepts are also briefly introduced>), I won't say much here (a lot of concepts introduced in it, the following explanations are also used, you can get familiar with it first)

and the certified manager
AuthenticationManager
We have also introduced it in <SpringSecurity Core Concepts>, and its implementation class is usually
ProviderManager
, So let's take a look
ProviderManager
How the source code is implemented

public class ProviderManager implements AuthenticationManager , MessageSourceAware , InitializingBean { //~ Static fields/initializers //=============================== ================================================== ==== private static final Log logger = LogFactory.getLog(ProviderManager.class); //~ Instance fields //========================================= ================================================== === private AuthenticationEventPublisher eventPublisher = new NullEventPublisher(); private List<AuthenticationProvider> providers = Collections.emptyList(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private AuthenticationManager parent; private boolean eraseCredentialsAfterAuthentication = true ; public Authentication authenticate (Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null ; AuthenticationException parentException = null ; Authentication result = null ; Authentication parentResult = null ; boolean debug = logger.isDebugEnabled(); //Get an authentication provider for (AuthenticationProvider provider: getProviders()) { if (!provider.supports(toTest)) { continue ; } try { //do authentication operation result = provider.authenticate(authentication); if (result != null ) { copyDetails(authentication, result); break ; } } //If there is a problem, an exception will be thrown, which will be handled by ExceptionTransactionFilter catch (AccountStatusException e) { prepareException(e, authentication); //SEC-546: Avoid polling additional providers if auth failure is due to //invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null ) { //Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { //ignore as we will throw below if no other exception occurred prior to //calling parent and the parent //may throw ProviderNotFound even though a provider in the child already //handled the request } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null ) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { //Authentication is complete. Remove credentials and other secret data //from authentication ((CredentialsContainer) result).eraseCredentials(); } //If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent //This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null ) { eventPublisher.publishAuthenticationSuccess(result); } return result; } //Parent was null, or didn't authenticate (or throw an exception). if (lastException == null ) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound" , new Object[] {toTest.getName() }, "No AuthenticationProvider found for {0}" )); } //If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent //This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null ) { prepareException(lastException, authentication); } throw lastException; } } Copy code

Known from the code,

ProviderManager
Will traverse all
AuthenticationProvider collection,
The real authentication logic is
AuthenticationProvider
Do

we continue to look at
AuthenticationProvider
The implementation class

we pick a common implementation class
DaoAuthenticationProvider
, Through its name, we know that this class is related to the database. We still analyze its parent class as before

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider , InitializingBean , MessageSourceAware { protected final Log logger = LogFactory.getLog(getClass()); //~ 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 DefaultAuthenticationChecks ( PostAuthenticationChecks ) private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); public Authentication authenticate (Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports" , "Only UsernamePasswordAuthenticationToken is supported" )); //Determine username String username = (authentication.getPrincipal() == null )? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true ; //Get user information from the cache UserDetails user = this .userCache.getUserFromCache(username); if (user == null ) { cacheWasUsed = false ; try { //Re-pulling user information, such as from the database, there are subclasses to achieve user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug( "User'" + username + "'not found" ); 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 { //Determine whether the account is locked or unavailable, etc. preAuthenticationChecks.check(user); //Password comparison, implemented by subclasses additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { //There was a problem, so try again after checking //we're using latest data (ie not from the cache) cacheWasUsed = false ; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } //Password expiration verification postAuthenticationChecks.check(user); if (!cacheWasUsed) { this .userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //No problem, return an Authentication object return createSuccessAuthentication(principalToReturn, authentication, user); } } Copy code

So let's roughly understand the logic of the parent class

  • Get user information from the cache, the default implementation class is
    NullUserCache
    , Returns null
  • Obtain user information from the logic written by the subclass
  • Do some pre-checks on user information
  • Do password comparison, this part is also implemented by subclasses
  • Post-check user information
  • No problem, return one
    Authentication
    Object


Let's take a look at the subclass logic

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //~ Static fields/initializers //================================== ================================================== /** * The plaintext password used to perform * PasswordEncoder#matches(CharSequence, String)} on when the user is * not found to avoid SEC-2056. */ private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword" ; //~ Instance fields //========================================= ================================================== === private PasswordEncoder passwordEncoder; /** * The password used to perform * { @link PasswordEncoder#matches(CharSequence, String)} on when the user is * not found to avoid SEC-2056. This is necessary, because some * { @link PasswordEncoder} implementations will short circuit if the password is not * in a valid format. */ private volatile String userNotFoundEncodedPassword; //The realization logic class of user information acquisition private UserDetailsService userDetailsService; private UserDetailsPasswordService userDetailsPasswordService; public DaoAuthenticationProvider () { setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } //~ Methods //===================================================================================================================================================================================================================== ================================================== ========== @SuppressWarnings("deprecation") protected void additionalAuthenticationChecks (UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null ) { logger.debug( "Authentication failed: no credentials provided" ); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } String presentedPassword = authentication.getCredentials().toString(); //Password comparison if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug( "Authentication failed: password does not match stored value" ); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } } protected void doAfterPropertiesSet () throws Exception { Assert.notNull( this .userDetailsService, "A UserDetailsService must be set" ); } //Get user information protected final UserDetails retrieveUser (String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //Get user information UserDetails loadedUser = this .getUserDetailsService().loadUserByUsername(username); if (loadedUser == null ) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation" ); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } } @Override protected Authentication createSuccessAuthentication (Object principal, Authentication authentication, UserDetails user) { boolean upgradeEncoding = this .userDetailsPasswordService != null && this .passwordEncoder.upgradeEncoding(user.getPassword()); if (upgradeEncoding) { String presentedPassword = authentication.getCredentials().toString(); String newPassword = this .passwordEncoder.encode(presentedPassword); user = this .userDetailsPasswordService.updatePassword(user, newPassword); } return super .createSuccessAuthentication(principal, authentication, user); } } Copy code

Let's explain one more

UserDetailsService
Commonly used implementation classes
JdbcDaoImpl
, Look at it
loadUserByUsername

@Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { List<UserDetails> users = loadUsersByUsername(username); if (users.size() == 0 ) { this .logger.debug( "Query returned no results for user'" + username + "'" ); throw new UsernameNotFoundException( this .messages.getMessage( "JdbcDaoImpl.notFound" , new Object[] {username }, "Username {0} not found" )); } //Get user information UserDetails user = users.get( 0 ); //contains no GrantedAuthority[] Set<GrantedAuthority> dbAuthsSet = new HashSet<>(); if ( this .enableAuthorities) { dbAuthsSet.addAll(loadUserAuthorities(user.getUsername())); } if ( this .enableGroups) { dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername())); } List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet); addCustomAuthorities(user.getUsername(), dbAuths); if (dbAuths.size() == 0 ) { this .logger.debug( "User'" + username + "'has no authorities and will be treated as'not found'" ); throw new UsernameNotFoundException( this .messages.getMessage( "JdbcDaoImpl.noAuthority" , new Object[] {username }, "User {0} has no GrantedAuthority" )); } return createUserDetails(username, user, dbAuths); } //Find user information from the database protected List<UserDetails> loadUsersByUsername (String username) { return getJdbcTemplate().query( this .usersByUsernameQuery, new String[] {username }, new RowMapper<UserDetails>() { @Override public UserDetails mapRow (ResultSet rs, int rowNum) throws SQLException { String username = rs.getString( 1 ); String password = rs.getString( 2 ); boolean enabled = rs.getBoolean( 3 ); return new User(username, password, enabled, true , true , true , AuthorityUtils.NO_AUTHORITIES); } }); } //Create User object protected UserDetails createUserDetails (String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) { String returnUsername = userFromUserQuery.getUsername(); if (! this .usernameBasedPrimaryKey) { returnUsername = username; } return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(), true , true , true , combinedAuthorities); } Copy code

and so

JdbcDaoImpl
It is to find out the user name, password, authority and other information from the database to form a
User
Object returned to
UserDetailService


So we can imitate Spring and implement one ourselves
UserDetailsService
To meet certain business needs

@Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private RoleService roleService; @Override public void save (SysUser user) { userDao.save(user); } @Override public List<SysUser> findAll () { return userDao.findAll(); } @Override public Map<String, Object> toAddRolePage (Integer id) { List<SysRole> allRoles = roleService.findAll(); List<Integer> myRoles = userDao.findRolesByUid(id); Map<String, Object> map = new HashMap<>(); map.put( "allRoles" , allRoles); map.put( "myRoles" , myRoles); return map; } @Override public void addRoleToUser (Integer userId, Integer[] ids) { userDao.removeRoles(userId); for (Integer rid: ids) { userDao.addRoles(userId, rid); } } @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if (sysUser == null ) { return null ; } List<SimpleGrantedAuthority> list = new ArrayList<>(); List<SysRole> roles = sysUser.getRoles(); List<SimpleGrantedAuthority> collect = roles.stream() .map(SysRole::getRoleName) .map(SimpleGrantedAuthority:: new ) .collect(Collectors.toList()); //{noop} No encryption authentication UserDetails userDetails = new User(username, sysUser.getPassword(), sysUser.getStatus() == 1 , true , true , true , collect); //UserDetails userDetails = new User(username, "{noop}"+sysUser.getPassword(), collect); return userDetails; } } Copy code

Then this class is added to the Security configuration class

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } //... } Copy code



reference: