This is the 26th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
The premise is introduced
This article introduces the principles of Spring Security, which will be of some reference value to the actual business development. I hope it will be helpful to you. In order to meet our needs with Spring Security, it is best to understand its principle, so that we can expand at will. This article mainly records the basic operation process of Spring Security.
The mechanism of filters
Spring Security basically implements configured authentication, permission authentication, and logout through filters.
Spring Security registers a filter FilterChainProxy in the Servlet’s filter chain, which proxies requests to Spring Security’s own filter chains, each of which matches a number of urls. If there is a match, the corresponding filter is executed. The filter chain is sequential, and only the first matching filter chain is executed for a request.
Spring Security’s configuration is essentially adding, removing, and modifying filters.By default, the 15 filters injected by the system correspond to different configuration requirements.
Control filter UsernamePasswordAuthenticationFilter certification
Then we will focus on the analysis of UsernamePasswordAuthenticationFilter this filter, he is used to login using the username and password authentication filters, but in many cases, we more than just a simple user name and password to log in, and can be used in the third party authorization login, This time we need to use a custom filter, of course, here is not to do the details, just say how to inject a custom filter.
@Override
protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(...) ; . }Copy the code
Identity authentication process
There are a few basic concepts we need to understand before we begin the authentication process:
SecurityContextHolder
SecurityContextHolder stores the SecurityContext object. SecurityContextHolder is a storage proxy with three storage modes:
- MODE_THREADLOCAL: The SecurityContext is stored in the thread.
- MODE_INHERITABLETHREADLOCAL: The SecurityContext is stored in the thread, but the child thread can get the SecurityContext in the parent thread.
- MODE_GLOBAL: SecurityContext is the same in all threads.
The SecurityContextHolder uses MODE_THREADLOCAL mode by default, and the SecurityContext is stored in the current thread.
The SecurityContextHolder object can be retrieved directly from the current thread without being passed the displayed argument.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Copy the code
Authentication
Authentication indicates the identity of the current user. What is authentication? For example, a set of user names and passwords is authentication, as is the wrong user name and password, but Spring Security will fail.
The Authentication interface
public interface Authentication extends Principal.Serializable {
Collection getAuthorities(a);
Object getCredentials(a);
Object getDetails(a);
Object getPrincipal(a);
boolean isAuthenticated(a);
void setAuthenticated(boolean isAuthenticated);
}
Copy the code
-
GetAuthorities () : Obtains user authority, which is typical of obtaining information about the user’s role.
-
GetCredentials () : Obtains the authentication information of the user. Generally, the authentication information is obtained, such as the password. However, the authentication information is removed once the login is successful.
-
GetDetails () : Gets additional information about the user, such as IP address, latitude and longitude, and so on
-
GetPrincipal () : Gets the user’s identity information, the user name in the unauthenticated case, and the UserDetails in the authenticated case.
-
IsAuthenticated () : obtains whether the current Authentication isAuthenticated
-
SetAuthenticated: Indicates whether the current Authentication is authenticated
AuthenticationManager ProviderManager AuthenticationProvider
The three are easy to distinguish:
- AuthenticationManager: The main purpose is to complete the identity authentication process.
- ProviderManager: Concrete implementation class for the AuthenticationManager interface
- ProviderManager has a collection property provider that records the AuthenticationProvider object
- The AuthenticationProvider interface class has two methods
public interface AuthenticationProvider {
// Implement specific authentication logic, authentication failure throws the corresponding exception
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// Whether the Authentication class supports the Authentication of this Authentication
boolean supports(Class authentication);
}
Copy the code
The next step is to traverse the providers collection in the ProviderManager and find the appropriate AuthenticationProvider to authenticate.
UserDetailsService UserDetails
There is only one simple method in the UserDetailsService interface
public interface UserDetailsService {
// Find the corresponding UserDetails object based on the user name
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code
Perform certification process
Slowly analysis when running to UsernamePasswordAuthenticationFilter filters.
The first is to enter its parent class AbstractAuthenticationProcessingFilter doFilter () method
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {...If yes, perform the following authentication; if no, skip
if(! requiresAuthentication(request, response)) { chain.doFilter(request, response);return; }... Authentication authResult;try {
/ / the key methods to realize the Authentication logic and return to Authentication, by its subclasses UsernamePasswordAuthenticationFilter implementation
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// Authentication failed to be invoked
unsuccessfulAuthentication(request, response, failed);
return;
}catch (AuthenticationException failed) {
// Authentication failed to be invoked
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// Authentication succeeded
successfulAuthentication(request, response, chain, authResult);
}
Copy the code
Handling logic of authentication failure
protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException { SecurityContextHolder.clearContext(); . rememberMeServices.loginFail(request, response);// This handler handles the failed interface jump and response logic
failureHandler.onAuthenticationFailure(request, response, failed);
}
Copy the code
The default configuration of processing handler is SimpleUrlAuthenticationFailureHandler failure, can be customized.
public class SimpleUrlAuthenticationFailureHandler implements
AuthenticationFailureHandler {...public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {
// If the URL fails to jump is not configured, it responds with an error
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}else {
/ / otherwise
// The cache is abnormal
saveException(request, exception);
// Redirect the abnormal page in different ways according to whether it is redirected or forwarded
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}else {
logger.debug("Redirecting to "+ defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); }}}// If the cache is abnormal, forwarding is stored in request and redirection is stored in session
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}else {
HttpSession session = request.getSession(false);
if(session ! =null|| allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,exception); }}}}Copy the code
Here is a small extension: use the system error processing handler, specify the URL of authentication failure to jump, in MVC corresponding URL method can get error information from request or session through key, feedback to the front end.
Authentication success processing logic
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {...// It is important to note that the return of Authentication is saved in the thread's 'SecurityContext'
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher ! =null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// This handler is used to complete the page jump
successHandler.onAuthenticationSuccess(request, response, authResult);
}
Copy the code
Here is the success of the default configuration processing handler SavedRequestAwareAuthenticationSuccessHandler, code doesn’t do elaborate on the inside, it is to jump to the specified certification after successful interface, can be customized.
Identity Details
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 String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true; .// Start the authentication logic
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 a simple AuthenticationToken with the username and password submitted from the front end
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// The actual authentication logic is handed over to Authenticate (..) The method is done, and then look down
return this.getAuthenticationManager().authenticate(authRequest); }}Copy the code
The source breakpoint trace shows that the final parsing is done by the AuthenticationManager interface implementation class ProviderManager
public class ProviderManager implements AuthenticationManager.MessageSourceAware.InitializingBean {...privateList providers = Collections.emptyList(); .public Authentication authenticate(Authentication authentication)
throws AuthenticationException {...// Go through all the AuthenticationProviders to find the right one to complete the authentication
for (AuthenticationProvider provider : getProviders()) {
if(! provider.supports(toTest)) {continue; }...try {
/ / for a specific authentication logic, is used here to DaoAuthenticationProvider, remember to look down the specific logic
result = provider.authenticate(authentication);
if(result ! =null) {
copyDetails(authentication, result);
break; }}catch. }...throwlastException; }}Copy the code
DaoAuthenticationProvider
DaoAuthenticationProvider inherited from AbstractUserDetailsAuthenticationProvider implement AuthenticationProvider interface
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider.InitializingBean.MessageSourceAware {...private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = newDefaultPostAuthenticationChecks(); .public Authentication authenticate(Authentication authentication)
throws AuthenticationException {...// Get the submitted user name
String username = (authentication.getPrincipal() == null)?"NONE_PROVIDED" : authentication.getName();
// Find the UserDetails from the cache based on the user name
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// retrieveUser is passed if there is none in cache (..) The realization of the method to find the (see below DaoAuthenticationProvider)
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}catch. }try {
// Check before comparison, for example, the account has some status information (whether locked, expired...)
preAuthenticationChecks.check(user);
/ / subclass implementation than rules (see below the realization of the DaoAuthenticationProvider)
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) 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,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if(! cacheWasUsed) {this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// Regenerate the detailed Authentication object based on some information about the final user and return it
return createSuccessAuthentication(principalToReturn, authentication, user);
}
// The generation is dependent on the subclass implementation
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
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
returnresult; }}Copy the code
Next we’ll look at the inside of the DaoAuthenticationProvider three important method, comparison method, ultimately return need than populated UserDetails object as well as the production method of Authentication.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {...// Password comparison
@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();
// Use PasswordEncoder to compare passwords. Note: This parameter can be customized
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")); }}// Run the UserDetailsService command to obtain the UserDetails
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// Run the UserDetailsService command to obtain the UserDetails
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); }}// Generates the Authentication returned after the Authentication succeeds, and records the identity information
@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 the code