SpringSecurity自定义登录过滤链分析

前言

我用SpringSecurity用的挺多的,但是基本还是只掌握了用法,对它的认证流程其实还是比较懵逼的,借助这个机会,我重新学习了一下SpringSecurity,对它有了更深层次的理解。

SpringSecurity认证流程图

SpringSecurity自定义登录过滤链分析_第1张图片

1 登录过滤链分析

1.1 UsernamePasswordAuthenticationFilter

/**
 * 处理提交的认证,首先调用AuthenticationProcessingFilter,参数是username和password
 * Processes an authentication form submission. Called
 * {@code AuthenticationProcessingFilter} prior to Spring Security 3.0.
 * 

* Login forms must present two parameters to this filter: a username and password. The * default parameter names to use are contained in the static fields * {@link #SPRING_SECURITY_FORM_USERNAME_KEY} and * {@link #SPRING_SECURITY_FORM_PASSWORD_KEY}. The parameter names can also be changed by * setting the {@code usernameParameter} and {@code passwordParameter} properties. *

* This filter by default responds to the URL {@code /login}. * * @author Ben Alex * @author Colin Sampaleanu * @author Luke Taylor * @since 3.0 */ public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== 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 // =================================================================================================== //设置父类的RequestMatcher public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } // ~ Methods // ======================================================================================================== //只能是post方法 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(); //构建UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * Enables subclasses to override the composition of the password, such as by * including additional values and a separator. *

* This might be used for example if a postcode/zipcode was required in addition to * the password. A delimiter such as a pipe (|) should be used to separate the * password and extended value(s). The AuthenticationDao will need to * generate the expected password in a corresponding manner. *

* * @param request so that request attributes can be retrieved * * @return the password that will be presented in the Authentication * request token to the AuthenticationManager */
@Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } /** * Enables subclasses to override the composition of the username, such as by * including additional values and a separator. * * @param request so that request attributes can be retrieved * * @return the username that will be presented in the Authentication * request token to the AuthenticationManager */ @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } /** * 提供这个方法,以便子类可以配置放入身份验证请求的详细信息属性中的内容。 * Provided so that subclasses may configure what is put into the authentication * request's details property. * * @param request that an authentication request is being created for * @param authRequest the authentication request object that should have its details * set */ protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Sets the parameter name which will be used to obtain the username from the login * request. * * @param usernameParameter the parameter name. Defaults to "username". */ public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } /** * Sets the parameter name which will be used to obtain the password from the login * request.. * * @param passwordParameter the parameter name. Defaults to "password". */ public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } /** * Defines whether only HTTP POST requests will be allowed by this filter. If set to * true, and an authentication request is received which is not a POST request, an * exception will be raised immediately and authentication will not be attempted. The * unsuccessfulAuthentication() method will be called as if handling a failed * authentication. *

* Defaults to true but may be overridden by subclasses. */ public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return usernameParameter; } public final String getPasswordParameter() { return passwordParameter; } }

UsernamePasswordAuthenticationFilter认证核心逻辑是将post请求传递过来的useranmepassword(当然可以通过配置改变默认的要求)组装成的UsernamePasswordAuthenticationToken交给ProviderManager进行验证,代码层面上是调用this.getAuthenticationManager().authenticate(authRequest)

UsernamePasswordAuthenticationFilter其实是抽象类AbstractAuthenticationProcessingFilter的实现类,认识整个流程离不开它的父类。

AbstractAuthenticationProcessingFilter

/**
 * Abstract processor of browser-based HTTP-based authentication requests.
 *
 * 

Authentication Process

* filter需要设置manager去处理tokens * The filter requires that you set the authenticationManager property. An * AuthenticationManager is required to process the authentication request tokens * created by implementing classes. *

* 如果请求符合RequestMatcher的内容,filter将会拦截请求,尝试认证 * This filter will intercept a request and attempt to perform authentication from that * request if the request matches the * {@link #setRequiresAuthenticationRequestMatcher(RequestMatcher)}. *

* 身份认证由attemptAuthentication方法处理,子类必须实现这个方法 * Authentication is performed by the * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse) * attemptAuthentication} method, which must be implemented by subclasses. * *

Authentication Success

* * 如果认证成功,这个Authentication将会被放到SecurityContext中 * If authentication is successful, the resulting {@link Authentication} object will be * placed into the SecurityContext for the current thread, which is * guaranteed to have already been created by an earlier filter. *

* The configured {@link #setAuthenticationSuccessHandler(AuthenticationSuccessHandler) * AuthenticationSuccessHandler} will then be called to take the redirect to the * appropriate destination after a successful login. The default behaviour is implemented * in a {@link SavedRequestAwareAuthenticationSuccessHandler} which will make use of any * DefaultSavedRequest set by the ExceptionTranslationFilter and * redirect the user to the URL contained therein. Otherwise it will redirect to the * webapp root "/". You can customize this behaviour by injecting a differently configured * instance of this class, or by using a different implementation. *

* See the * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * method for more information. * *

Authentication Failure

* * 如果认证失败,会委托配置的AuthenticationFailureHandler去处理错误信息返回给客户端。默认实现是SimpleUrlAuthenticationFailureHandler,返回401 * If authentication fails, it will delegate to the configured * {@link AuthenticationFailureHandler} to allow the failure information to be conveyed to * the client. The default implementation is {@link SimpleUrlAuthenticationFailureHandler} * , which sends a 401 error code to the client. It may also be configured with a failure * URL as an alternative. Again you can inject whatever behaviour you require here. * *

Event Publication

* 认证成功,一个InteractiveAuthenticationSuccessEvent事件会被放到application context中 * If authentication is successful, an {@link InteractiveAuthenticationSuccessEvent} will * be published via the application context. No events will be published if authentication * was unsuccessful, because this would generally be recorded via an * {@code AuthenticationManager}-specific application event. * *

Session Authentication

* 提供可选择的SessionAuthenticationStrategy,将会在attemptAuthentication()成功后备立即调用 * The class has an optional {@link SessionAuthenticationStrategy} which will be invoked * immediately after a successful call to {@code attemptAuthentication()}. Different * implementations * {@link #setSessionAuthenticationStrategy(SessionAuthenticationStrategy) can be * injected} to enable things like session-fixation attack prevention or to control the * number of simultaneous sessions a principal may have. * * @author Ben Alex * @author Luke Taylor */
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(); // ~ Constructors // =================================================================================================== /** * @param defaultFilterProcessesUrl the default value for filterProcessesUrl. */ protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) { setFilterProcessesUrl(defaultFilterProcessesUrl); } /** * Creates a new instance * * @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to * determine if authentication is required. Cannot be null. */ protected AbstractAuthenticationProcessingFilter( RequestMatcher requiresAuthenticationRequestMatcher) { Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null"); this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher; } // ~ Methods // ======================================================================================================== @Override public void afterPropertiesSet() { Assert.notNull(authenticationManager, "authenticationManager must be specified"); } /** * 调用requiresAuthentication方法去决定这个请求是否是认证请求,是否被这个filter处理 * Invokes the * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) * requiresAuthentication} method to determine whether the request is for * authentication and should be handled by this filter. If it is an authentication * request, the * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse) * attemptAuthentication} will be invoked to perform the authentication. There are * then three possible outcomes: * 三种可能的情况 *
    * 一个Authentication对象被返回,然后调用配置的SessionAuthenticationStrategy处理,再然后是successfulAuthentication *
  1. An Authentication object is returned. The configured * {@link SessionAuthenticationStrategy} will be invoked (to handle any * session-related behaviour such as creating a new session to protect against * session-fixation attacks) followed by the invocation of * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * method
  2. * 抛出AuthenticationException异常,调用unsuccessfulAuthentication处理 *
  3. An AuthenticationException occurs during authentication. The * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) * unsuccessfulAuthentication} method will be invoked
  4. * 返回空值,表示认证流程未完成 *
  5. Null is returned, indicating that the authentication process is incomplete. The * method will then return immediately, assuming that the subclass has done any * necessary work (such as redirects) to continue the authentication process. The * assumption is that a later request will be received by this method where the * returned Authentication object is not null. *
*/
//核心代码,认证流程 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { 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) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); } /** * 指示此filter是否应尝试处理当前调用的登录请求 * Indicates whether this filter should attempt to process a login request for the * current invocation. *

* It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in https://host/myapp/index.html;jsessionid=blah) * before matching against the filterProcessesUrl property. *

* Subclasses may override for special requirements, such as Tapestry integration. * * @return true if the filter should attempt authentication, * false otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); } /** * 实际执行身份验证的方法 * Performs actual authentication. *

* The implementation should do one of the following: *

    *
  1. Return a populated authentication token for the authenticated user, indicating * successful authentication
  2. *
  3. Return null, indicating that the authentication process is still in progress. * Before returning, the implementation should perform any additional work required to * complete the process.
  4. *
  5. Throw an AuthenticationException if the authentication process fails
  6. *
* * @param request from which to extract parameters and perform the authentication * @param response the response, which may be needed if the implementation has to do a * redirect as part of a multi-stage authentication process (such as OpenID). * * @return the authenticated user token, or null if authentication is incomplete. * * @throws AuthenticationException if authentication fails. */
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException; /** * 默认认证成功的实现 * Default behaviour for successful authentication. *
    * 在SecurityContextHolder添加Authentication对象 *
  1. Sets the successful Authentication object on the * {@link SecurityContextHolder}
  2. * 通知配置的RememberMeServices登录成功 *
  3. Informs the configured RememberMeServices of the successful login
  4. * 通过已配置的ApplicationEventPublisher触发InteractiveAuthenticationSuccessEvent *
  5. Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured * ApplicationEventPublisher
  6. * 委托额外的操作给AuthenticationSuccessHandler *
  7. Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.
  8. *
* * 子类可以重写这个方法,在认证成功后去继续FilterChain * Subclasses can override this method to continue the {@link FilterChain} after * successful authentication. * @param request * @param response * @param chain * @param authResult the object returned from the attemptAuthentication * method. * @throws IOException * @throws ServletException */
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); } 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. *
    *
  1. Clears the {@link SecurityContextHolder}
  2. *
  3. Stores the exception in the session (if it exists or * allowSesssionCreation is set to true)
  4. *
  5. Informs the configured RememberMeServices of the failed login
  6. *
  7. Delegates additional behaviour to the {@link AuthenticationFailureHandler}.
  8. *
*/
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); } rememberMeServices.loginFail(request, response); failureHandler.onAuthenticationFailure(request, response, failed); } protected AuthenticationManager getAuthenticationManager() { return authenticationManager; } public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * Sets the URL that determines if authentication is required * * @param filterProcessesUrl */ public void setFilterProcessesUrl(String filterProcessesUrl) { setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher( filterProcessesUrl)); } public final void setRequiresAuthenticationRequestMatcher( RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); this.requiresAuthenticationRequestMatcher = requestMatcher; } public RememberMeServices getRememberMeServices() { return rememberMeServices; } public void setRememberMeServices(RememberMeServices rememberMeServices) { Assert.notNull(rememberMeServices, "rememberMeServices cannot be null"); this.rememberMeServices = rememberMeServices; } /** * Indicates if the filter chain should be continued prior to delegation to * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * , which may be useful in certain environment (such as Tapestry applications). * Defaults to false. */ public void setContinueChainBeforeSuccessfulAuthentication( boolean continueChainBeforeSuccessfulAuthentication) { this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication; } public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } protected boolean getAllowSessionCreation() { return allowSessionCreation; } public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } /** * The session handling strategy which will be invoked immediately after an * authentication request is successfully processed by the * AuthenticationManager. Used, for example, to handle changing of the * session identifier to prevent session fixation attacks. * * @param sessionStrategy the implementation to use. If not set a null implementation * is used. */ public void setSessionAuthenticationStrategy( SessionAuthenticationStrategy sessionStrategy) { this.sessionStrategy = sessionStrategy; } /** * Sets the strategy used to handle a successful authentication. By default a * {@link SavedRequestAwareAuthenticationSuccessHandler} is used. */ public void setAuthenticationSuccessHandler( AuthenticationSuccessHandler successHandler) { Assert.notNull(successHandler, "successHandler cannot be null"); this.successHandler = successHandler; } public void setAuthenticationFailureHandler( AuthenticationFailureHandler failureHandler) { Assert.notNull(failureHandler, "failureHandler cannot be null"); this.failureHandler = failureHandler; } protected AuthenticationSuccessHandler getSuccessHandler() { return successHandler; } protected AuthenticationFailureHandler getFailureHandler() { return failureHandler; } }

AbstractAuthenticationProcessingFilter中的流程就是主要的认证流程,认证完成后将得到的Authentication放到SecurityContext中,并且调用AuthenticationSuccessHandler完成成功后的处理(可以在这里配置自己需要的AuthenticationSuccessHandler)

1.2 ProviderManager

/**
 * Iterates an {@link Authentication} request through a list of
 * {@link AuthenticationProvider}s.
 *
 * 

* AuthenticationProvider会按照顺序依次尝试,直到返回非空。如果随后的provider成功的验证了请求,前面的异常会被忽略并将使用成功的认证结果 * AuthenticationProviders are usually tried in order until one provides a * non-null response. A non-null response indicates the provider had authority to decide * on the authentication request and no further providers are tried. If a subsequent * provider successfully authenticates the request, the earlier authentication exception * is disregarded and the successful authentication will be used. If no subsequent * provider provides a non-null response, or a new AuthenticationException, * the last AuthenticationException received will be used. If no provider * returns a non-null response, or indicates it can even process an * Authentication, the ProviderManager will throw a * ProviderNotFoundException. A parent {@code AuthenticationManager} can also * be set, and this will also be tried if none of the configured providers can perform the * authentication. This is intended to support namespace configuration options though and * is not a feature that should normally be required. *

* The exception to this process is when a provider throws an * {@link AccountStatusException}, in which case no further providers in the list will be * queried. * * Post-authentication, the credentials will be cleared from the returned * {@code Authentication} object, if it implements the {@link CredentialsContainer} * interface. This behaviour can be controlled by modifying the * {@link #setEraseCredentialsAfterAuthentication(boolean) * eraseCredentialsAfterAuthentication} property. * *

Event Publishing

*

* 认证事件发布被委派给配置的AuthenticationEventPublisher(默认是空实现) * Authentication event publishing is delegated to the configured * {@link AuthenticationEventPublisher} which defaults to a null implementation which * doesn't publish events, so if you are configuring the bean yourself you must inject a * publisher bean if you want to receive events. The standard implementation is * {@link DefaultAuthenticationEventPublisher} which maps common exceptions to events (in * the case of authentication failure) and publishes an * {@link org.springframework.security.authentication.event.AuthenticationSuccessEvent * AuthenticationSuccessEvent} if authentication succeeds. If you are using the namespace * then an instance of this bean will be used automatically by the <http> * configuration, so you will receive events from the web part of your application * automatically. *

* Note that the implementation also publishes authentication failure events when it * obtains an authentication result (or an exception) from the "parent" * {@code AuthenticationManager} if one has been set. So in this situation, the parent * should not generally be configured to publish events or there will be duplicates. * * * @author Ben Alex * @author Luke Taylor * * @see DefaultAuthenticationEventPublisher */ 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 ProviderManager(List<AuthenticationProvider> providers) { this(providers, null); } public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; checkState(); } // ~ Methods // ======================================================================================================== public void afterPropertiesSet() { checkState(); } private void checkState() { if (parent == null && providers.isEmpty()) { throw new IllegalArgumentException( "A parent AuthenticationManager or a list " + "of AuthenticationProviders is required"); } } /** * 尝试去认证传递过来的Authentication对象 * Attempts to authenticate the passed {@link Authentication} object. *

* The list of {@link AuthenticationProvider}s will be successively tried until an * AuthenticationProvider indicates it is capable of authenticating the * type of Authentication object passed. Authentication will then be * attempted with that AuthenticationProvider. *

* 如果有多个AuthenticationProvider支持传递过来的Authentication,第一个认证成功的决定result,覆盖任何早期的异常 * If more than one AuthenticationProvider supports the passed * Authentication object, the first one able to successfully * authenticate the Authentication object determines the * result, overriding any possible AuthenticationException * thrown by earlier supporting AuthenticationProviders. * On successful authentication, no subsequent AuthenticationProviders * will be tried. * If authentication was not successful by any supporting * AuthenticationProvider the last thrown * AuthenticationException will be rethrown. * * @param authentication the authentication request object. * * @return a fully authenticated object including credentials. * * @throws AuthenticationException if authentication fails. */ 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(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { 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. 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; } @SuppressWarnings("deprecation") private void prepareException(AuthenticationException ex, Authentication auth) { eventPublisher.publishAuthenticationFailure(ex, auth); } /** * Copies the authentication details from a source Authentication object to a * destination one, provided the latter does not already have one set. * * @param source source authentication * @param dest the destination authentication object */ private void copyDetails(Authentication source, Authentication dest) { if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) { AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest; token.setDetails(source.getDetails()); } } public List<AuthenticationProvider> getProviders() { return providers; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } public void setAuthenticationEventPublisher( AuthenticationEventPublisher eventPublisher) { Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null"); this.eventPublisher = eventPublisher; } /** * If set to, a resulting {@code Authentication} which implements the * {@code CredentialsContainer} interface will have its * {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called * before it is returned from the {@code authenticate()} method. * * @param eraseSecretData set to {@literal false} to retain the credentials data in * memory. Defaults to {@literal true}. */ public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) { this.eraseCredentialsAfterAuthentication = eraseSecretData; } public boolean isEraseCredentialsAfterAuthentication() { return eraseCredentialsAfterAuthentication; } private static final class NullEventPublisher implements AuthenticationEventPublisher { public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) { } public void publishAuthenticationSuccess(Authentication authentication) { } } }

ProviderManager会拿到复数的AuthenticationProvider进行检查,如果能够支持处理,就使用这个AuthenticationProvider进行认证

1.3 DaoAuthenticationProvider

/**
 * An {@link AuthenticationProvider} implementation that retrieves user details from a
 * {@link UserDetailsService}.
 *
 * @author Ben Alex
 * @author Rob Winch
 */
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;

	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();

		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() {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
	}

	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 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);
	}

	private void prepareTimingAttackProtection() {
		if (this.userNotFoundEncodedPassword == null) {
			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
		}
	}

	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
		if (authentication.getCredentials() != null) {
			String presentedPassword = authentication.getCredentials().toString();
			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
		}
	}

	/**
	 * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
	 * not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
	 *
	 * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
	 * types.
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsPasswordService(
			UserDetailsPasswordService userDetailsPasswordService) {
		this.userDetailsPasswordService = userDetailsPasswordService;
	}
}

根据我们自己实现的UserDetailsService来得到UserDetails并与传过来的Authentication比较,成功后构建UsernamePasswordAuthenticationToken返回

AbstractUserDetailsAuthenticationProvider

/**
 * 基础的AuthenticationProvider允许子类复写并且和UserDetails一起,这个类是回应UsernamePasswordAuthenticationToken认证请求
 * A base {@link AuthenticationProvider} that allows subclasses to override and work with
 * {@link org.springframework.security.core.userdetails.UserDetails} objects. The class is
 * designed to respond to {@link UsernamePasswordAuthenticationToken} authentication
 * requests.
 *
 * 

* 成功的验证后,会创建UsernamePasswordAuthenticationToken返回,token包含UserDetails和代表用户username的String * Upon successful validation, a UsernamePasswordAuthenticationToken will be * created and returned to the caller. The token will include as its principal either a * String representation of the username, or the {@link UserDetails} that was * returned from the authentication repository. Using String is appropriate * if a container adapter is being used, as it expects String representations * of the username. Using UserDetails is appropriate if you require access to * additional properties of the authenticated user, such as email addresses, * human-friendly names etc. As container adapters are not recommended to be used, and * UserDetails implementations provide additional flexibility, by default a * UserDetails is returned. To override this default, set the * {@link #setForcePrincipalAsString} to true. *

* 缓存是通过存储UserDetails对象到来UserCache中处理的,保证接下来一样的username能够被验证,不需要调用UserDetailsService来获取。默认的存储NullUserCache * Caching is handled by storing the UserDetails object being placed in the * {@link UserCache}. This ensures that subsequent requests with the same username can be * validated without needing to query the {@link UserDetailsService}. It should be noted * that if a user appears to present an incorrect password, the {@link UserDetailsService} * will be queried to confirm the most up-to-date password was used for comparison. * Caching is only likely to be required for stateless applications. In a normal web * application, for example, the SecurityContext is stored in the user's session * and the user isn't reauthenticated on each request. The default cache implementation is * therefore {@link NullUserCache}. * * @author Ben Alex */ 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 DefaultPostAuthenticationChecks(); private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); // ~ Methods // ======================================================================================================== /** * 允许子类对返回的UserDetails进行额外的验证 * Allows subclasses to perform any additional checks of a returned (or cached) * UserDetails 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 UserDetails and/or * UsernamePasswordAuthenticationToken, these should also appear in this * method. * * @param userDetails as retrieved from the * {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or * UserCache * @param authentication the current request that needs to be authenticated * * @throws AuthenticationException AuthenticationException if the credentials could * not be validated (generally a BadCredentialsException, an * AuthenticationServiceException) */ protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken 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(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; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //这里调用实现类的retrieveUser方法,这里是DaoAuthenticationProvider 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 { preAuthenticationChecks.check(user); 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(); } return createSuccessAuthentication(principalToReturn, authentication, user); } /** * 创建一个成功的Authentication对象 * Creates a successful {@link Authentication} object. *

* 子类可以重写 * Protected so subclasses can override. *

*

* 子类将保存原本用户提供的(没有加盐和加密)的Authentication对象 * Subclasses will usually store the original credentials the user supplied (not * salted or encoded passwords) in the returned Authentication object. *

* * @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 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); 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; } /** * 允许子类真正的去检索UserDetails从一个特定的实现的位置 * Allows subclasses to actually retrieve the UserDetails from an * implementation-specific location, with the option of throwing an * AuthenticationException 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 UserDetails). *

* Subclasses are not required to perform any caching, as the * AbstractUserDetailsAuthenticationProvider will by default cache the * UserDetails. The caching of UserDetails 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 UserDetails will ever be cached) or * ensure subclasses implement * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} * to compare the credentials of a cached UserDetails with subsequent * authentication requests. *

*

* 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. *

* * @param username The username to retrieve * @param authentication The authentication request, which subclasses may * need to perform a binding-based retrieval of the UserDetails * * @return the user information (never null - instead an exception should * the thrown) * * @throws AuthenticationException if the credentials could not be validated * (generally a BadCredentialsException, an * AuthenticationServiceException or * UsernameNotFoundException) */
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; public void setForcePrincipalAsString(boolean forcePrincipalAsString) { this.forcePrincipalAsString = forcePrincipalAsString; } /** * 如果username和密码不正确,默认会抛出BadCredentialsException,设置为false会抛出UsernameNotFoundException代替前面的异常 * By default the AbstractUserDetailsAuthenticationProvider throws a * BadCredentialsException if a username is not found or the password is * incorrect. Setting this property to false will cause * UsernameNotFoundExceptions to be thrown instead for the former. Note * this is considered less secure than throwing BadCredentialsException * for both exceptions. * * @param hideUserNotFoundExceptions set to false if you wish * UsernameNotFoundExceptions to be thrown instead of the non-specific * BadCredentialsException (defaults to true) */ 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<?> authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); } protected UserDetailsChecker getPreAuthenticationChecks() { return preAuthenticationChecks; } /** * Sets the policy will be used to verify the status of the loaded * UserDetails before 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; } public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; } private class DefaultPreAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isAccountNonLocked()) { logger.debug("User account is locked"); throw new LockedException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } if (!user.isEnabled()) { logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } if (!user.isAccountNonExpired()) { logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } } private class DefaultPostAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isCredentialsNonExpired()) { logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired")); } } } }

你可能感兴趣的:(Spring,Security)