史上最简单的Spring Security教程(二十七):AuthenticationManager默认实现之ProviderManager详解
Spring Security 框架中的另一个重要接口 AuthenticationManager
, 被设计用于处理 Authentication
请求。
与 AuthenticationProvider
接口一致,AuthenticationManager
接口中有且只有一个方法,即authenticate(Authentication authentication)
方法。
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
该方法与 AuthenticationProvider
中的 authenticate 方法声明及功能完全一致,返回包含凭据的完整身份验证对象 authentication
。但是,如果 AuthenticationProvider 不支持给定的 Authentication
的话,该方法可能会返回 null
。在此情况下,下一个支持 authentication
的 AuthenticationProvider 将会被尝试。
AuthenticationManager
接口的默认实现为 ProviderManager
,其逻辑也不复杂。
首先,便是调用 AuthenticationProvider
中的 supports(Class<?> authentication)
方法,判断是否支持当前的 Authentication
请求。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
只有支持当前 Authentication
请求的 AuthenticationProvider
才会继续后续逻辑处理。
然后,便是调用 AuthenticationProvider
中的 authenticate(Authentication authentication)
方法进行身份认证。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
......
try {
result = provider.authenticate(authentication);
......
}
如果认证成功且返回的结果不为 null
,则执行 authentication details 的拷贝逻辑。
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
......
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,则会通过Spring事件发布器AuthenticationEventPublisher
发布异常事件。
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
......
private void prepareException(AuthenticationException ex, Authentication auth) {
eventPublisher.publishAuthenticationFailure(ex, auth);
}
如果异常为其它类型的 AuthenticationException
,则将此异常设置为 lastException
并返回。
catch (AuthenticationException e) {
lastException = e;
}
如果认证结果为 null
,且存在父 AuthenticationManager
,则调用父 AuthenticationManager
进行同样的身份认证操作,其处理逻辑基本同上。
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;
}
}
如果认证结果不为 null
,同时,此时的 eraseCredentialsAfterAuthentication
参数为 true
,且此时认证后的 Authentication
实现了 CredentialsContainer
接口,那么即调用 CredentialsContainer
接口的凭据擦除方法,即eraseCredentials,擦除相关凭据信息。
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;
}
其中,有一个防止重复发布 AuthenticationSuccessEvent
事件的处理,即 parentResult
为空。如果 parentResult
为 null
,则代表父 AuthenticationManager
不存在或者没有身份认证成功,也即没有发布过 AuthenticationSuccessEvent
事件。此时,便由此处发布 AuthenticationSuccessEvent
事件。
最后,便是对于 lastException
的相关处理。
如果 lastException
为 null
,则代表当前的 Authentication
并没有对应支持的 Provider。此时,便会抛出相应异常。
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
接下来,如同防止重复发布 AuthenticationSuccessEvent
事件的处理一样,也有一个防止 AbstractAuthenticationFailureEvent
事件重复发布的逻辑处理。如果 parentException 为 null
,则代表父AuthenticationManager
不存在、没有进行身份认证或者发布过 AbstractAuthenticationFailureEvent
事件,此时,便由此处发布 AbstractAuthenticationFailureEvent
事件。
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
最后,抛出 lastException
。
但是,抛出 lastException
之后呢?其实,是被另外一个 Filter 捕获并初始化到当前用户的 Request
中,感兴趣的朋友可以关注后续的文章,会有详细的解释。
如同之前介绍其它重要接口一样,我们了解了其详细的逻辑以后,不妨自己自定义一个 AuthenticationManager
的实现。我们先不实现复杂的逻辑,就针对 UsernamePasswordAuthenticationToken
相对应的 DaoAuthenticationProvider
来实现一个 AuthenticationManager
。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
if (provider.supports(toTest)) {
......
try {
result = provider.authenticate(authentication);
}
......
catch (AuthenticationException e) {
lastException = e;
}
......
} else {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
......
throw lastException;
}
此 AuthenticationManager
只接受 DaoAuthenticationProvider
或者其子类,可以单纯的作为例子来看,不作其它意义。
其它详细源码,请参考文末源码链接,可自行下载后阅读。
我是银河架构师,十年饮冰,难凉热血,愿历尽千帆,归来仍是少年!
如果文章对您有帮助,请举起您的小手,轻轻【三连】,这将是笔者持续创作的动力源泉。当然,如果文章有错误,或者您有任何的意见或建议,请留言。感谢您的阅读!
源码
github
https://github.com/liuminglei/SpringSecurityLearning/tree/master/27
gitee
https://gitee.com/xbd521/SpringSecurityLearning/tree/master/27
上一篇: Python类的用法实例浅析
下一篇: PHP从FLV文件获取视频预览图的方法