The second part of the Spring Security series analyzes the authentication process
chapter
Spring Security is a series of simple introduction and practical
The second part of the Spring Security series analyzes the authentication process
Spring Security series 3: Custom SMS Login Authentication
The fourth in the Spring Security series uses JWT for authentication
Spring Security series # 5: User authorization for backend decoupage projects
Spring Security Series 6 authorization Process Analysis
Written before the demo, trust for beginners, there are a lot of confusion, and, now basically is the separation of front and back end project, like before that you want to configure the login page and jump page doesn’t work, usually we only need to write a login interface, and access to the front of the user name password, and then passed on to the security, The verification result is obtained from security, and the result is encapsulated and returned to the front end.
However, the premise of doing this is to have a certain understanding of the security authentication process, so we will not write any code today, just talk about the authentication process.
Login filter
Front end for the login operation, will perform UsernamePasswordAuthenticationFilter filter, this filter less code:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login"."POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter(a) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && ! request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: "+ request.getMethod()); } String username = obtainUsername(request); username = (username ! =null)? username :""; username = username.trim(); String password = obtainPassword(request); password = (password ! =null)? password :"";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest); }}Copy the code
A path DEFAULT_ANT_PATH_REQUEST_MATCHER is passed to the parent class. Based on its definition, it is possible to intercept an interface with path /login and type POST. We will see what its parent class does with this in a moment.
AttemptAuthentication method, four things are done:
- Determine if the request type is POST and throw an exception if it is not.
- from
request
The user name and password are obtained fromusername
andpassword
(So the fields passed from the front end must be these two) - Assemble the extracted username and password into one
UsernamePasswordAuthenticationToken
- Handed in internally
AuthenticationManager
To authenticate and return authentication information
The AuthenticationManager will come later. Now we know that this method is to get the username and password from the front end and verify it. AttemptAuthentication method is called when? This have to explore its superclass AbstractAuthenticationProcessingFilter agent, first take a look at class inheritance relationships:
Can see AbstractAuthenticationProcessingFilter inherited the Filter for the servlet Filter we should be very familiar with, not to force, go directly to the class of the doFilter method:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//1. Verify that the URL is the one we need
if(! requiresAuthentication(request, response)) { chain.doFilter(request, response);return;
}
try {
//2. Perform the actual authentication and return the filled authentication token of the authenticated user, indicating that the authentication is successful
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// The authentication process is still ongoing
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//3. After the verification is successful, publish the event to notify the listener to handle, and do the callback, jump to the successful page
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failedunsuccessfulAuthentication(request, response, ex); }}Copy the code
We analyze the above steps step by step, first verifying the URL:
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s".this.requiresAuthenticationRequestMatcher));
}
return false;
}
Copy the code
The requiresAuthenticationRequestMatcher is coming from a constructor, which means the place will determine whether for the login url, if it is, for the following authentication.
Authentication is an abstract method whose implementation is handed over to its subclasses:
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
Copy the code
This method is the method to realize we were in the UsernamePasswordAuthenticationFilter, validation completed returns an Authentication object. The Authentication class describes the information about the current user, including the list of user permissions and user details. Common implementation classes are UsernamePasswordAuthenticationToken, interested can look at the source.
We now reach the time to call the attemptAuthentication method, but some things are done after calling this method. Let’s go ahead here.
Save login Status
Handling after successful authentication:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher ! =null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// The callback we configured earlier in securityConfig
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
Copy the code
The SecurityContextHolder here gets the SecurityContext, and the calling method stores the user information. Since storing user information must be associated with the user thread, SecurityContextHolder provides the following modes of operation:
- SecurityContextHolder. MODE_THREADLOCAL (default) : using threadlocal, user information will be available for all methods under the thread, a thread binding and strategy, very suitable for the Servlet Web applications.
- SecurityContextHolder. MODE_GLOBAL: use independent applications
- SecurityContextHolder. MODE_INHERITABLETHREADLOCAL: with the same thread safety signs
Using the default ThreadLocal policy, we can easily retrieve the current logged-in user information from the SecurityContextHolder:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
Copy the code
Behind the event release and event monitoring, interested brothers can go to see the source code, here is no longer the study.
Login Authentication Starting Point
The entire call relationship above is as follows:
Filter -> AbstractAuthenticationProcessingFilter -> UsernamePasswordAuthenticationFilter -> AuthenticationManager -> successHandler
As you can see, the actual authentication is handed over to the AuthenticationManager class, which is the core interface related to authentication and the starting point for everything. Common authentication processes are handled by the Implementation class ProviderManager of AuthenticationManager.
Let’s first look at the AuthenticationManager interface definition as follows:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
Copy the code
ProviderManager class implementation:
@Override
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;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if(! provider.supports(toTest)) {continue;
}
try {
// Each AuthenticationProvider is authenticated until authentication succeeds
result = provider.authenticate(authentication);
if(result ! =null) {
copyDetails(authentication, result);
break; }}catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch(AuthenticationException ex) { lastException = ex; }}// If the above authentication fails, the parent class AuthenticationProvider is authenticated
if (result == null && this.parent ! =null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// 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 ex) { parentException = ex; lastException = ex; }}if(result ! =null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Clear the password after authentication is complete
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager has been successfully authenticated, the event is published. If the event has already been published, it is checked to prevent repeated delivery.
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Throw an exception if the process is not authenticated.
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound".new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
Copy the code
ProviderManager has a list of AuthenticationProviders, which are authenticated in order, and only one AuthenticationProvider is required to be authenticated to be considered a successful login. Passwords are cleared for security. If all providers fail validation, an exception is thrown.
The AuthenticationProvider interface is defined as follows:
public interface AuthenticationProvider {
/** * Perform the same authentication as the following contract * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
* .
* @paramAuthentication Object to be authenticated *@returnReturns the validated object. May be null *@throwsAuthenticationException Throws an exception */
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/** * Returns true if this AuthenticationProvide supports authenticating the Authentication object. * Returning true does not necessarily support authentication, it simply indicates that it can support a more careful evaluation of it. * /
boolean supports(Class
authentication);
}
Copy the code
It mainly has the following implementations:
DaoAuthenticationProvider
: The default implementation, uses the account and password authentication mode, obtains authentication data from the database.AnonymousAuthenticationProvider
: Login authentication mode for touristsRememberMeAuthenticationProvider
: Obtains the authentication mode from cookies
The database obtains user information
We usually get user data validation from the database, so let’s pick the first one. DaoAuthenticationProvider class inheritance structure is as follows:
First look at AbstractUserDetailsAuthenticationProvider authenticate the implementation:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports"."Only UsernamePasswordAuthenticationToken is supported"));
// Get the user name from authentication
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// Try to fetch the user from the cache
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// If there is none in the cache, the abstract method is executed, and the concrete subclass gets the user data
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// Check whether the user account is disabled
this.preAuthenticationChecks.check(user);
// Passes to a specific subclass to verify that the identity is correct
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if(! cacheWasUsed) {throw ex;
}
// 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, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if(! cacheWasUsed) {this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// Create user credentials
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
Copy the code
To summarize what this class does:
- The user name was obtained from authentication
- Query user information according to user name, query details to subclass
- Checking User Status
- Verifying that the user is correctly identified (password is correct) is also handed over to subclasses
- Create user credentials and return
Among them, the second step and step 4 the realization of the need to have a look at the DaoAuthenticationProvider:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
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 newInternalAuthenticationServiceException(ex.getMessage(), ex); }}}@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }}Copy the code
At this point, it is very clear that the loadUserByUsername method of UserDetailsService is called and username is passed in. We implemented the UserDetailsService interface ourselves and retrieved the user from the database. After the query out perform additionalAuthenticationChecks method is verified by passwordEncoder password is correct. At this point, the whole verification process is finished.
Add links:
Filter -> AbstractAuthenticationProcessingFilter -> UsernamePasswordAuthenticationFilter -> AuthenticationManager -> ProviderManager -> AuthenticationProvider -> DaoAuthenticationProvider -> successHandler
Other filters
The whole process of the above are our is based on the analysis of the UsernamePasswordAuthenticationFilter to do, in fact, in the Spring security still has a lot of the filter, through the filter, Spring Security not only implements authentication logic, but also implements common Web attack protection.
Here are some common filters:
filter | instructions |
---|---|
SecurityContextPersistenceFilter | Use to put the SecurityContext into the session |
UsernamePasswordAuthenticationFilter | Filter for login authentication |
RememberMeAuthenticationFilter | This is done through cookiesRemember that I Function Filter |
AnonymousAuthenticationFilter | Anonymous authentication processing filter. When the authentication information in the SecurityContextHolder is empty, an anonymous user is created and stored in the SecurityContextHolder |
SessionManagementFilter | Session management Filter, persisting user login information, can be saved to the session, cookie or redis |
ExceptionTranslationFilter | Exception handling filter, mainly to intercept the exception thrown in the subsequent filter (FilterSecurityInterceptor) operation. |
FilterSecurityInterceptor | A security interceptor filter class that throws an exception when accessing a URL with insufficient permissions |
The order of these filters in the FilterChain is very important. For each system or user-defined filter, Spring Security requires that an order be specified for sorting. The order of filters for the system is defined in a FilterComparator class:
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(SwitchUserFilter.class, order.next());
}
Copy the code
If we want to customize the security filter, we must also specify whether to add it before or after that filter. And we’re going to use this later in the game.
Expansion: after the user login authentication, how to determine the user does not need to authenticate again when entering the next time? Interested friends can study the source code.
conclusion
In-depth source code view, may be a lot of class responsibility is not too clear, the next to make a summary.
-
AbstractAuthenticationProcessingFilter: Filter-based authentication request abstract processing class requires passing in a request path. If the request path matches, the filter intercepts the request and encapsulates the parameters as a token class, which is passed to the AuthenticationManager. Try to perform authentication (login authentication).
-
AuthenticationManager: Management class for user authentication. All authentication requests are implemented by submitting a token to the Authenticate () method of the AuthenticationManager. Of course it doesn’t do the work, the verification action is done by AuthenticationManager forwarding the request to the specific implementation class. Based on the results of the implementation feedback, the specific Handler is called to give feedback to the user.
-
ProviderManager: Management class that provides user authentication. Requests submitted by AuthenticationManager are forwarded to this class, which loops to the Provider for authentication until one authentication succeeds. If none succeeds, the user is not authenticated.
-
AuthenticationProvider: Authentication implementation class, a corresponding an authentication provider implementation, such as user account password, the corresponding implementation class is DaoAuthenticationProvider, if be oauth2 login, then have a OAuth2Provider implementation.
-
UserDetailService: class to obtain user information. The user authenticates the user information saved in the system in the AuthenticationProvider and performs authentication. We need to implement this interface ourselves to return user information.
Certification flow chart: