Based on the study of the source code, only part of the source code is discussed. The picture of Shang Silicon Valley teacher is used for reference, and the explanation of the source code is written in the code,
It is divided into three parts: authentication process, permission access process + spring security request to share authentication information
1. Certification process
It mainly relies on the filter: UsernamePasswordAuthenticationFilter, which is used for user login authentication and so on.
(1). The method AbstractAuthenticationProcessingFilter in the parent class is called
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {}
The filter depends on its doFilter, and the key points will be commented in the code
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) {//1. Judge the submission method chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult;//Class used to encapsulate validation information try {//2. Call attemptAuthentication in the subclass UsernamePasswordAuthenticationFilter to obtain the data of the form for authentication. After success, package the result into authResult authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } //3. Configure session policies, such as setting the maximum concurrent number of sessions, etc sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); //4 - 4.1 here means that if the authentication fails, the following method will be called unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success //4-4.2 for the processing after successful authentication, continuechainbeforesuccessful authentication is false by default, if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }
Process:
1. Judge the submission method
2. Call attemptAuthentication in the subclass UsernamePasswordAuthenticationFilter to obtain the data of the form for authentication. After success, package the result into authResult
3. Configure the session policy,
4
4.1 here, it means that if the authentication fails, the following method will be called
4.2 for the processing after successful authentication, continuechainbeforesuccessful authentication is false by default,
2. Go deep into the attemptAuthentication method in UsernamePasswordAuthenticationFilter:
Source code
The specific reason is the ghost of attemptAuthentication. The source code:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) {//1. Judge whether it is a post submission throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //If yes, get the user name and password submitted by the form String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } //obtain username = username.trim(); //Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //Setting related information // Allow subclasses to set the "details" property setDetails(request, authRequest); //Call authenticate to authenticate return this.getAuthenticationManager().authenticate(authRequest); }
Two methods need to be called: obtainUsername obtainPassword: (only obtainUsername is listed)
@Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); }
obtainUsername---->this.usernameParameter:
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";//Notice the value here 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;
Summarize the points for attention and process:
1. Judge whether it is submitted by post. If so, get the user name and password submitted by the form
2. Get the data, mark it as unauthenticated, set some information in the request into the object, and call authenticate for authentication (it will call the method in userDetailsService written by itself for authentication, that is, check the data)
Specific code:
1.1 judge whether it is a post submission. If so, get the user name and password submitted by the form. At the same time, the details are the page. The name submitted by the form must be written as "username" and "password", otherwise security cannot recognize it. The specific reason is that the source code above has been written
if (postOnly && !request.getMethod().equals("POST")) {//1. Judge whether it is a post submission throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //If yes, get the user name and password submitted by the form String username = obtainUsername(request); String password = obtainPassword(request);
2. Get the data, mark it as unauthenticated, set some information in the request into the object, and call authenticate for authentication (it will call the method in userDetailsService written by itself for authentication, that is, check the data)
//obtain username = username.trim(); //Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //Set relevant information // Allow subclasses to set the "details" property setDetails(request, authRequest); //Call authenticate to authenticate return this.getAuthenticationManager().authenticate(authRequest);
2.1 there is a method: set the request information into the object, setDetails
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); }
public void setDetails(Object details) { this.details = details; }
2.2. About the unauthorized problem in the source code:
In the source code: //Get a token object, put the obtained data into the token, and mark the authentication status as unauthenticated (this step is required in the initial figure) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
Specific methods of source code:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; //Mark as not certified setAuthenticated(false); }
(Note: the following points are actually detailed explanations for the specific operations in attemptAuthentication)
3. Go deeper and look at the relevant codes of UsernamePasswordAuthenticationToken:
There are two main points: unsuccessful authentication (that is, the construction method in 2.2 above has been described, and the authentication is successful below)
Successful certification:
/** * This constructor should only be used by <code>AuthenticationManager</code> or * <code>AuthenticationProvider</code> implementations that are satisfied with * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>) * authentication token. * * @param principal * @param credentials * @param authorities */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; //Mark as certified super.setAuthenticated(true); // must use super, as we override }
4.UsernamePasswordAuthenticationToken inherits from abstractauthenticationtoken, which implements the interface: Authentication view the interface below: Authentication:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities();//Permission set Object getCredentials();//Password verification Object getDetails();//User details Object getPrincipal();// boolean isAuthenticated();//Authorized void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;//Set whether to be authorized }
The above is in-depth in the process of reading the source code. Now go back to the original source code to explore the relevant contents of authorization
5. See the last step in attemptAuthentication, authenticate authentication:
Pass in the unauthenticated information for identity authentication
return this.getAuthenticationManager().authenticate(authRequest);
1.getAuthenticationManager—
protected AuthenticationManager getAuthenticationManager() { return authenticationManager; }
Method returns AuthenticationManager
2.AuthenticationManager
public interface AuthenticationManager {}
It is found that it is an interface. Find the implementation class
3. Find his implementation class: ProviderManager. The authentication is implemented here
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {}
Specific operation of authenticate;
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //Gets the Authentication type passed in Usernamepasswordauthenticationtoken class Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); //The previous code obtained the content through the iterator, and now it is a for loop for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) {//Determine whether the current AuthenticationProvider is applicable to //Usernamepasswordauthenticationtoken Authentication of type class continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } //The AuthenticationProvider adapted to the current authentication method was successfully found try {//Call the authenticate() method of the AuthenticationProvider that is successfully found to adapt to the current authentication method to start authentication //If the Authentication is successful, an Authentication object marked as authenticated will be returned result = provider.authenticate(authentication); if (result != null) { //After successful Authentication, copy the details information in the incoming Authentication object to the authenticated Authentication object copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. //Authentication failed. Use the parent type AuthenticationManager for authentication 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) {//After successful authentication, take out the relevant sensitive information in the result and require the relevant classes to implement the credentialscontainer interface if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //The removal process is to call the eraseCredentials method of the CredentialsContainer interface ((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 //The above words have also been confirmed, and relevant information will be released. If you don't, find your father. Anyway, if you want to have it, the following ones will not be repeated //Publishing authentication success events if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). //Authentication failure: throw a message 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;
1. Get authentication first, that is, the previous UsernamePasswordAuthenticationToken
2. Get the content through the iterator, and now get the authentication content through the for loop
6. Go back to the beginning: find the method of authentication failure and success (authentication success just now) and find it in its parent class
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } //Encapsulate the user object with successful authentication SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); } /** * Default behaviour for unsuccessful authentication. * <ol> * <li>Clears the {@link SecurityContextHolder}</li> * <li>Stores the exception in the session (if it exists or * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li> * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li> * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li> * </ol> */ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } //Here is to remember that my related operation failed rememberMeServices.loginFail(request, response); failureHandler.onAuthenticationFailure(request, response, failed); }
Let's take a look at the permission access process: it mainly involves two filters: FilterSecurityInterceptor and exceptiontranslation filter
1.ExceptionTranslationFilter catches the thrown exception and handles it
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { //Direct release requested by the current end chain.doFilter(request, response); this.logger.debug("Chain processed normally"); } catch (IOException var9) { throw var9; } catch (Exception var10) { //Catch and handle the thrown exception. AuthenticationException: no permission Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10); RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } if (ase == null) { if (var10 instanceof ServletException) { throw (ServletException)var10; } if (var10 instanceof RuntimeException) { throw (RuntimeException)var10; } throw new RuntimeException(var10); } if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10); } this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase); } }
2.FilterSecurityInterceptor
Judge whether the current request can access the resource according to the resource access permission
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); this.invoke(fi); }
invoke: execution method:
public void invoke(FilterInvocation fi) throws IOException, ServletException { if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { if (fi.getRequest() != null && this.observeOncePerRequest) { fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, (Object)null); } }
Core part:
//Judge whether the current request can access the resource according to the resource access permission. Here, you need to go through spring MVC to carry out relevant operations InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse());//Access resource filter } finally { super.finallyInvocation(token); }
3. Sharing authentication information between spring security requests
Look at the source code:
public interface SecurityContext extends Serializable { //The essence is to encapsulate Authentication Authentication getAuthentication(); /** * Changes the currently authenticated principal, or removes the authentication * information. * * @param authentication the new <code>Authentication</code> token, or * <code>null</code> if no further authentication information should be stored */ void setAuthentication(Authentication authentication); }
Look at the construction method in its implementation class, there are encapsulation steps:
public class SecurityContextImpl implements SecurityContext { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private Authentication authentication; public SecurityContextImpl() {} public SecurityContextImpl(Authentication authentication) { this.authentication = authentication; }
Let's look at SecurityContextHolder
Many things have been done. The main one is to bind the operation to the current thread:
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
Second: find a new securityContext object in the current thread and return it if any. If not, create a new securityContext object:
public static SecurityContext getContext() { return strategy.getContext(); }
Finally, respond through the SecurityContextPersistenceFilter:
This filter is at the front of all filters. It binds Authentication and session. See his dofilter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (request.getAttribute("__spring_security_scpf_applied") != null) { chain.doFilter(request, response); } else { boolean debug = this.logger.isDebugEnabled(); request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE); if (this.forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { this.logger.debug("Eagerly created session: " + session.getId()); } } //When the request comes, check whether there is a SecurityContext object in the current session. If there is, it will return. If not, it will create a new SecurityContext and return HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder); boolean var13 = false; try { var13 = true; //Put the SecurityContext object obtained above into it SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); var13 = false; } finally { if (var13) { //This is where the response is started and the SecurityContext is fetched for the request SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); //Remove securityContext SecurityContextHolder.clearContext(); this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied"); if (debug) { this.logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } } SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); SecurityContextHolder.clearContext(); this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied"); if (debug) { this.logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } }