架构
Spring Security的整体架构,官网文档有介绍:https://docs.spring.io/spring-security/reference/5.7/servlet/architecture.html
友情提示:可以使用Edge浏览器打开,翻译一下,帮助理解,英文阅读能力好的话忽略此提示。
在Spring Security的过滤器中有一套过滤器链,官网把他们按照顺序列出来了
ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
这些Filter的顺序很重要,上面排列的顺序也是他们调用的顺序。
其中
UsernamePasswordAuthenticationFilter
就是 使用用户名和密码认证时要通过的一个过滤器
认证过滤器
先看一下 UsernamePasswordAuthenticationFilter 这个类的方法结构
其中的 attemptAuthentication 方法就是认证逻辑
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
在之前的配置中,就有涉及这个地方的配置,比如登录的url、请求方法、用户名的参数名、密码的参数名。
这里会从request获取对应的用户名和密码,然后构建一个未认证的UsernamePasswordAuthentiactionToken ,最后一步进行认证。
这个认证逻辑也是有套路的,通过ProviderManager进行认证,自身拿不到认证结果就从parent那里拿,一层层往上委托认证,最后委托到DaoAuthenticationProvider,从我们自定义的UserDetailsService中获取认证对象,至于我们的UserDetailsService何时注入到这个对象中,那就是另一个故事了,可以在set方法里面打一个断点,启动时就可以看到方法栈,看到类构建过程,这里就不看了。
认证成功
当认证成功,会回到 AbstractPreAuthenticatedProcessingFilter 的 successfulAuthentication 方法
设置身份上下文,触发认证成功事件,调用成功重定向处理器
可以看到认证成功重定向url就是之前我们设置的。
当然Spring Security的配置方式很多,配置组合也很多,这只是其中一种。
认证失败
ProviderManager的authenticate方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// ... 省略一大堆认证逻辑
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then 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;
}
如果没有拿到认证结果,而且还有parent,向上委托认证,单亲委托
一直往上找,找到老祖宗还没拿到认证结果,而且没有异常产生
那么就会产生一个异常ProviderNotFoundException
如果parent没有异常,那就发出认证失败事件
最终会抛出lastException异常
在认证过程中也会抛出异常,毕竟常见的BadCredentialsException
DaoAuthenticationProvider中additionalAuthenticationChecks方法会抛出
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
最后赋值给lastException,抛出到上层,到异常处理器。
具体逻辑在 AbstractAuthenticationProcessingFilter 的 doFilter方法中的异常catch块
最终调用了 unsuccessfulAuthentication 方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
重定向的url也是之前配置的。
这里的成功和失败的执行逻辑和官网的逻辑流程图吻合。