前言
我用SpringSecurity用的挺多的,但是基本还是只掌握了用法,对它的认证流程其实还是比较懵逼的,借助这个机会,我重新学习了一下SpringSecurity,对它有了更深层次的理解。
SpringSecurity认证流程图
1 登录过滤链分析
1.1 UsernamePasswordAuthenticationFilter
/**
* 处理提交的认证,首先调用AuthenticationProcessingFilter,参数是username和password
* Processes an authentication form submission. Called
* {@code AuthenticationProcessingFilter} prior to Spring Security 3.0.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 <code>AuthenticationDao</code> will need to
* generate the expected password in a corresponding manner.
* </p>
*
* @param request so that request attributes can be retrieved
*
* @return the password that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@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 <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@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
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> 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
请求传递过来的useranme
和password
(当然可以通过配置改变默认的要求)组装成的UsernamePasswordAuthenticationToken
交给ProviderManager
进行验证,代码层面上是调用this.getAuthenticationManager().authenticate(authRequest)
UsernamePasswordAuthenticationFilter
其实是抽象类AbstractAuthenticationProcessingFilter
的实现类,认识整个流程离不开它的父类。
AbstractAuthenticationProcessingFilter
/**
* Abstract processor of browser-based HTTP-based authentication requests.
*
* <h3>Authentication Process</h3>
* filter需要设置manager去处理tokens
* The filter requires that you set the <tt>authenticationManager</tt> property. An
* <tt>AuthenticationManager</tt> is required to process the authentication request tokens
* created by implementing classes.
* <p>
* 如果请求符合RequestMatcher的内容,filter将会拦截请求,尝试认证
* This filter will intercept a request and attempt to perform authentication from that
* request if the request matches the
* {@link #setRequiresAuthenticationRequestMatcher(RequestMatcher)}.
* <p>
* 身份认证由attemptAuthentication方法处理,子类必须实现这个方法
* Authentication is performed by the
* {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} method, which must be implemented by subclasses.
*
* <h4>Authentication Success</h4>
*
* 如果认证成功,这个Authentication将会被放到SecurityContext中
* If authentication is successful, the resulting {@link Authentication} object will be
* placed into the <code>SecurityContext</code> for the current thread, which is
* guaranteed to have already been created by an earlier filter.
* <p>
* 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
* <tt>DefaultSavedRequest</tt> set by the <tt>ExceptionTranslationFilter</tt> 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.
* <p>
* See the
* {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
* method for more information.
*
* <h4>Authentication Failure</h4>
*
* 如果认证失败,会委托配置的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.
*
* <h4>Event Publication</h4>
* 认证成功,一个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.
*
* <h4>Session Authentication</h4>
* 提供可选择的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 <tt>filterProcessesUrl</tt>.
*/
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:
* 三种可能的情况
* <ol>
* 一个Authentication对象被返回,然后调用配置的SessionAuthenticationStrategy处理,再然后是successfulAuthentication
* <li>An <tt>Authentication</tt> 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</li>
* 抛出AuthenticationException异常,调用unsuccessfulAuthentication处理
* <li>An <tt>AuthenticationException</tt> occurs during authentication. The
* {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
* unsuccessfulAuthentication} method will be invoked</li>
* 返回空值,表示认证流程未完成
* <li>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 <tt>Authentication</tt> object is not null.
* </ol>
*/
//核心代码,认证流程
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.
* <p>
* It strips any parameters from the "path" section of the request URL (such as the
* jsessionid parameter in <em>https://host/myapp/index.html;jsessionid=blah</em>)
* before matching against the <code>filterProcessesUrl</code> property.
* <p>
* Subclasses may override for special requirements, such as Tapestry integration.
*
* @return <code>true</code> if the filter should attempt authentication,
* <code>false</code> otherwise.
*/
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
/**
* 实际执行身份验证的方法
* Performs actual authentication.
* <p>
* The implementation should do one of the following:
* <ol>
* <li>Return a populated authentication token for the authenticated user, indicating
* successful authentication</li>
* <li>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.</li>
* <li>Throw an <tt>AuthenticationException</tt> if the authentication process fails</li>
* </ol>
*
* @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.
* <ol>
* 在SecurityContextHolder添加Authentication对象
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* 通知配置的RememberMeServices登录成功
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* 通过已配置的ApplicationEventPublisher触发InteractiveAuthenticationSuccessEvent
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* 委托额外的操作给AuthenticationSuccessHandler
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* 子类可以重写这个方法,在认证成功后去继续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 <tt>attemptAuthentication</tt>
* 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.
* <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);
}
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 <code>false</code>.
*/
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
* <tt>AuthenticationManager</tt>. 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.
*
* <p>
* AuthenticationProvider会按照顺序依次尝试,直到返回非空。如果随后的provider成功的验证了请求,前面的异常会被忽略并将使用成功的认证结果
* <tt>AuthenticationProvider</tt>s 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 <code>AuthenticationException</code>,
* the last <code>AuthenticationException</code> received will be used. If no provider
* returns a non-null response, or indicates it can even process an
* <code>Authentication</code>, the <code>ProviderManager</code> will throw a
* <code>ProviderNotFoundException</code>. 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.
* <p>
* 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.
*
* <h2>Event Publishing</h2>
* <p>
* 认证事件发布被委派给配置的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 <tt><http></tt>
* configuration, so you will receive events from the web part of your application
* automatically.
* <p>
* 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.
* <p>
* The list of {@link AuthenticationProvider}s will be successively tried until an
* <code>AuthenticationProvider</code> indicates it is capable of authenticating the
* type of <code>Authentication</code> object passed. Authentication will then be
* attempted with that <code>AuthenticationProvider</code>.
* <p>
* 如果有多个AuthenticationProvider支持传递过来的Authentication,第一个认证成功的决定result,覆盖任何早期的异常
* If more than one <code>AuthenticationProvider</code> supports the passed
* <code>Authentication</code> object, the first one able to successfully
* authenticate the <code>Authentication</code> object determines the
* <code>result</code>, overriding any possible <code>AuthenticationException</code>
* thrown by earlier supporting <code>AuthenticationProvider</code>s.
* On successful authentication, no subsequent <code>AuthenticationProvider</code>s
* will be tried.
* If authentication was not successful by any supporting
* <code>AuthenticationProvider</code> the last thrown
* <code>AuthenticationException</code> 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.
*
* <p>
* 成功的验证后,会创建UsernamePasswordAuthenticationToken返回,token包含UserDetails和代表用户username的String
* Upon successful validation, a <code>UsernamePasswordAuthenticationToken</code> will be
* created and returned to the caller. The token will include as its principal either a
* <code>String</code> representation of the username, or the {@link UserDetails} that was
* returned from the authentication repository. Using <code>String</code> is appropriate
* if a container adapter is being used, as it expects <code>String</code> representations
* of the username. Using <code>UserDetails</code> 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
* <code>UserDetails</code> implementations provide additional flexibility, by default a
* <code>UserDetails</code> is returned. To override this default, set the
* {@link #setForcePrincipalAsString} to <code>true</code>.
* <p>
* 缓存是通过存储UserDetails对象到来UserCache中处理的,保证接下来一样的username能够被验证,不需要调用UserDetailsService来获取。默认的存储NullUserCache
* Caching is handled by storing the <code>UserDetails</code> 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 <tt>SecurityContext</tt> 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)
* <code>UserDetails</code> 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 <code>UserDetails</code> and/or
* <code>UsernamePasswordAuthenticationToken</code>, these should also appear in this
* method.
*
* @param userDetails as retrieved from the
* {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or
* <code>UserCache</code>
* @param authentication the current request that needs to be authenticated
*
* @throws AuthenticationException AuthenticationException if the credentials could
* not be validated (generally a <code>BadCredentialsException</code>, an
* <code>AuthenticationServiceException</code>)
*/
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.
* <p>
* 子类可以重写
* Protected so subclasses can override.
* </p>
* <p>
* 子类将保存原本用户提供的(没有加盐和加密)的Authentication对象
* Subclasses will usually store the original credentials the user supplied (not
* salted or encoded passwords) in the returned <code>Authentication</code> object.
* </p>
*
* @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 <code>UserDetails</code> from an
* implementation-specific location, with the option of throwing an
* <code>AuthenticationException</code> 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 <code>UserDetails</code>).
* <p>
* Subclasses are not required to perform any caching, as the
* <code>AbstractUserDetailsAuthenticationProvider</code> will by default cache the
* <code>UserDetails</code>. The caching of <code>UserDetails</code> 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 <code>UserDetails</code> will ever be cached) or
* ensure subclasses implement
* {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
* to compare the credentials of a cached <code>UserDetails</code> with subsequent
* authentication requests.
* </p>
* <p>
* 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.
* </p>
*
* @param username The username to retrieve
* @param authentication The authentication request, which subclasses <em>may</em>
* need to perform a binding-based retrieval of the <code>UserDetails</code>
*
* @return the user information (never <code>null</code> - instead an exception should
* the thrown)
*
* @throws AuthenticationException if the credentials could not be validated
* (generally a <code>BadCredentialsException</code>, an
* <code>AuthenticationServiceException</code> or
* <code>UsernameNotFoundException</code>)
*/
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 <code>AbstractUserDetailsAuthenticationProvider</code> throws a
* <code>BadCredentialsException</code> if a username is not found or the password is
* incorrect. Setting this property to <code>false</code> will cause
* <code>UsernameNotFoundException</code>s to be thrown instead for the former. Note
* this is considered less secure than throwing <code>BadCredentialsException</code>
* for both exceptions.
*
* @param hideUserNotFoundExceptions set to <code>false</code> if you wish
* <code>UsernameNotFoundException</code>s to be thrown instead of the non-specific
* <code>BadCredentialsException</code> (defaults to <code>true</code>)
*/
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
* <tt>UserDetails</tt> <em>before</em> 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"));
}
}
}
}