在学习Spring Cloud 时,遇到了受权服务oauth 相关内容时,老是只知其一;不知其二,所以决定先把Spring Security 、Spring Security Oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程当中增强印象和理解所撰写的,若有侵权请告知。git
项目环境:github
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
还记得上一篇讲解 受权过程 中提到@EnableWebSecurity 引用了 WebSecurityConfiguration 配置类 和 @EnableGlobalAuthentication 注解吗? 当时只是讲解了下 WebSecurityConfiguration 配置类 ,此次该轮到 @EnableGlobalAuthentication 配置了。web
查看 @EnableGlobalAuthentication 注解源码,咱们能够看到其引用了AuthenticationConfiguration 配置类。其中有一个方法值得咱们注意,那就是 getAuthenticationManager() (还记得受权过程当中调用了 AuthenticationManager().authenticate() 进行认证么?), 咱们来看下其源码内部大体逻辑:spring
public AuthenticationManager getAuthenticationManager() throws Exception {
......
// 1 调用 authenticationManagerBuilder 方法获取 authenticationManagerBuilder 对象,用于 build authenticationManager 对象
AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder(
this.objectPostProcessor, this.applicationContext);
.....
// 2 build 方法调用同受权过程当中的 webSecurity.build() 同样,都是经过父类 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 方法进行 build, 只是这里再也不是经过其子类 HttpSecurity.performBuild() ,而是经过 AuthenticationManagerBuilder.performBuild()
authenticationManager = authBuilder.build();
.......
return authenticationManager;
}
复制代码
根据源码咱们能够归纳其逻辑分2部分:数据库
- 一、 经过调用 authenticationManagerBuilder() 方法获取 authenticationManagerBuilder 对象
- 二、 调用authenticationManagerBuilder 对象的 build() 建立 authenticationManager 对象并返回
咱们再详细看下这个build的过程,能够发现其 build 调用跟受权过程当中build securityFilterChain 同样 都是经过 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 进行构建, 不过此次再也不是调用其子类 HttpSecurity.performBuild() 而是 AuthenticationManagerBuilder.performBuild() 。 咱们来看下 AuthenticationManagerBuilder.performBuild() 方法内部实现:缓存
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
// 1 建立了一个包含 authenticationProviders 参数 的 ProviderManager 对象
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
复制代码
这里咱们主要关注其内部 建立了一个包含 authenticationProviders 参数 的 ProviderManager (ProviderManager 是 AuthenticationManager 的实现类)对象并返回。安全
回过头,咱们来看下 AuthenticationManager 接口 源码:bash
public interface AuthenticationManager {
// 认证接口
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
复制代码
能够看到,内部就只有一个咱们在受权过程当中提到过的 authenticate(),其接口接收一个 Authentication(这个对象咱们也不陌生,以前受权过程当中提到过的 UsernamePasswordAuthrnticationToken 等都是其实现子类) 对象做为参数。app
至此认证的部分关键类或接口已经浮出水面了,它们分别是 AuthenticationManager 、ProviderManager、AuthenticationProvider、Authentication, 接下来咱们就围绕这几个类或接口进行剖析。ide
正如咱们以前看到的一项,它是整个认证的入口,其定义的认证接口 authenticate() 接收一个 Authentication 对象做为参数。AuthenticationManager 它只是提供了一个认证接口方法,由于在实际使用中,咱们不只有帐户密码的登陆方式,还有短信验证码登陆、邮箱登陆等等,因此它自己不作任何认证,其具体作认证的是 ProviderManager 子类,但正如咱们说过的认证方式有不少,若是仅仅依靠 ProviderManager 自己来实现 authenticate() 接口,那咱们要支持这么多认证方式不得写多少个 if 判断,并且之后若是咱们想要支持指纹登陆,那又不得不在这个方法内部加个if,这种不利于系统扩展的写法确定是不可取的,因此 ProviderManager 自己会维护一个List<AuthenticationProvider>列表 ,用于存放多种认证方式,而后经过委托的方式,调用 AuthenticationProvider 来真正实现认证逻辑的 。 而 Authentication 就是咱们须要认证的信息(固然不只仅只包括帐户信息),经过authenticate() 接口认证成功后返回的 Authentication 就是一个被标识认证成功的对象 。 这里为何要解释下 AuthenticationManager、ProviderManager、AuthenticationProvider 的关系,主要是一开始容易搞混它们,相信通过这样一段描述更容易理解了吧。。。
若是 没有看过源码的同窗可能会认为 Authentication 是一个类吧,可实际上它是一个 接口,其内部并未存在任何属性字段,它仅仅定义了和规范好了认证对象须要的接口方法,咱们来看看其定义的接口方法有哪些,分别又什么做用:
public interface Authentication extends Principal, Serializable {
// 1 获取权限信息(不能仅仅理解未角色权限,还有菜单权限等等),默认是GrantedAuthority接口的实现类
Collection<? extends GrantedAuthority> getAuthorities();
// 2 获取用户密码信息 ,认证成功后会被删除掉
Object getCredentials();
// 3 主要存放访问着的ip等信息
Object getDetails();
// 4 重点!! 最重要的身份信息。 大部分状况下是 UserDetails 接口的实现 类,好比 咱们 以前配置的 User 对象
Object getPrincipal();
// 5 是否定证(成功)
boolean isAuthenticated();
// 6 设置认证标识
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
复制代码
既然 Authentication 定义了这些接口方法,那么其子类实现确定都按照这个标准或者称之为规范定制了实现,这里就不罗列出其子类的具体实现了,有兴趣的同窗能够去看下 咱们最经常使用的 UsernamePasswordAuthenticationToken 实现(包括其 父类 AbstractAuthenticationToken)
它是 AuthenticationManager 的实现子类之一,也是咱们最经常使用的一个实现。正如咱们前面提到过的,其内部维护了 一个 List<AuthenticationProvider> 对象, 用于支持和扩展 多种形式的认证方式。咱们来看下 其 实现 authenticate() 的源码:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
// 1 经过 getProviders() 方法获取到内部维护的 List<AuthenticationProvider> 对象 并 经过遍历的方式 去 认证,只要认证成功 就 break
for (AuthenticationProvider provider : getProviders()) {
// 2 正如前面看到的有 不少 AuthenticationProvider 实现,若是每次都是验证失败后再掉用下一个 AuthenticationProvider 这种实现是否是很不高效? 因此 这里经过 supports() 方法来验证是否可使用 该 AuthenticationProvider 进行验证,不能够就直接换下一个
if (!provider.supports(toTest)) {
continue;
}
try {
// 3 重点,这里是 调用真实的认证方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
try {
// 4 前面都认证不成功,调用父类(严格意思不是调用父类,而是其余的 AuthenticationManager 实现类)认证方法
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 5 删除认证成功后的 密码信息,保证安全
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
复制代码
梳理下整个方法内部实现逻辑:
- 经过 getProviders() 方法获取到内部维护的 List 对象 并 经过遍历的方式 去 认证
- 经过 provider.supports() 方法 来验证是否可用当前的 AuthenticationProvider 进行验证,不能够就直接换下一个 ( 其实方法内部就是验证当前 的 Authentication 对象是否是其某个子类,好比 咱们最经常使用到的 DaoAuthenticationProvider 的 supports 方法就是判断当前 的 Authentication 是否是 UsernamePasswordAuthenticationToken )
- 经过 provider.authenticate() 调用 其真正的认证明现
- 若是 前面的全部 AuthenticationProvider 均不能认证成功,尝试调用 parent.authenticate() 方法 :调用父类(严格意思不是调用父类,而是其余的 AuthenticationManager 实现类)认证方法
- 最后 经过 ((CredentialsContainer) result).eraseCredentials() 删除认证成功后的 密码信息,保证安全
正如咱们想象的同样,AuthenticationProvider 是一个接口,自己定义了一个 和 AuthenticationManager 同样的 authenticate 认证接口方法,外加一个 supports() 用于 判别当前 Authentication 是否能够进行处理。
public interface AuthenticationProvider {
// 定义认证接口方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// 定义判断是否能够认证处理的接口方法
boolean supports(Class<?> authentication);
}
复制代码
这里咱们就拿咱们用得最多的一个 AuthenticationProvider 实现类 DaoAuthenticationProvider(注意,这里和UsernamePasswordAuthenticationFilter 相似,都是经过父类来实现接口,而后内部处理方法再调用 其 子类进行处理) 来看其内部 这2个抽象方法的实现:
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
复制代码
能够看到仅仅只是判断当前的 authentication 是否为 UsernamePasswordAuthenticationToken(或其子类)
// 1 注意这里的实现方法是 DaoAuthenticationProvider 的父类 AbstractUserDetailsAuthenticationProvider 实现的
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 2 从 authentication 中获取 用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 3 根据username 从缓存中获取 认证成功的 UserDetails 信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 4 若是缓存中没有用户信息 须要 获取用户信息(由 DaoAuthenticationProvider 实现 )
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
......
}
}
try {
// 5 前置检查帐户是否锁定,过时,冻结(由DefaultPreAuthenticationChecks类实现)
preAuthenticationChecks.check(user);
// 6 主要是验证 获取到的用户密码与传入的用户密码是否一致
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; } } // 7 后置检查用户密码是否 过时 postAuthenticationChecks.check(user); // 8 验证成功后的用户信息存入缓存 if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } // 9 从新建立一个 authenticated 为true (即认证成功)的 UsernamePasswordAuthenticationToken 对象并返回 return createSuccessAuthentication(principalToReturn, authentication, user); } 复制代码
梳理下authenticate(这里的方法的实现是由 AbstractUserDetailsAuthenticationProvider 提供的)方法内部实现逻辑:
- 从 入参 authentication 对象中获取到 username 信息
- (这里忽略缓存的处理) 调用 retrieveUser() 方法(由 DaoAuthenticationProvider 实现)根据 username 获取到 系统(通常来讲是从数据库中) 中获取到 UserDetails 对象
- 经过 preAuthenticationChecks.check() 方法检测 当前获取到的 UserDetails 是否过时、冻结、锁定(若是任意一个条件 为 true 将抛出 相应 的异常)
- 经过 additionalAuthenticationChecks() (由 DaoAuthenticationProvider 实现) 判断 密码是否一致
- 经过 postAuthenticationChecks.check() 检测 UserDetails 的密码是否过时
- 最后经过 createSuccessAuthentication() 从新建立一个 authenticated 为true (即认证成功)的 UsernamePasswordAuthenticationToken 对象并返回
虽然咱们知道其验证逻辑, 但其内部不少方法咱们不清楚其内部实现,以及这里新增的一个 关键认证类 UserDetails 是怎么设计的,如何验证其是否过时等等。
继续深刻看下 retrieveUser() 方法,首先咱们注意到其返回对象是一个 UserDetails,那么咱们先从 UserDetails 入手。
咱们先来看下 UserDetails 源码:
public interface UserDetails extends Serializable {
// 1 与 Authentication 的 同样,都是获取 权限信息
Collection<? extends GrantedAuthority> getAuthorities();
// 2 获取用户正确的密码
String getPassword();
// 3 获取帐户名
String getUsername();
// 4 帐户是否过时
boolean isAccountNonExpired();
// 5 帐户是否锁定
boolean isAccountNonLocked();
// 6 密码是否过时
boolean isCredentialsNonExpired();
// 7 帐户是否冻结
boolean isEnabled();
}
复制代码
从上面的 4,5,6,7 接口咱们就可以知道 preAuthenticationChecks.check() 和 postAuthenticationChecks.check() 是如何检测的了,这里2个方法的检测细节就再也不深究了,有兴趣的同窗能够看看源码,咱们只要知道检测失败会抛出异常就好了。
咋呼一看,这个UserDetails 和 Authentication 很类似,其实它们之间还真有关系,在createSuccessAuthentication() 传教Authentication 对象时,它的authorities 就是UserDetails 传入的。
retrieveUser() 方法是系统经过传入的帐户名获取对应的帐户信息的惟一方法,咱们来看下其内部源码逻辑:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 经过 UserDetailsService 的loadUserByUsername 方法 获取用户信息
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) {
......
}
}
复制代码
相信看到这里,一切都关联上了,这里的 UserDetailsService.loadUserByUsername() 就是咱们在 上一篇 受权过程当中 咱们本身实现的。 这里就再也不 贴出UserDetailsService 源码了。
还有additionalAuthenticationChecks() 密码验证没有讲到,这里简单提下,其内部就是经过 PasswordEncoder.matches() 方法进行密码匹配的。不过这里要注意一下,这里的 PasswordEncoder 在 Security 5 开始默认 替换成了 DelegatingPasswordEncoder 这里也是和咱们以前 讨论 loadUserByUsername 方法内部建立User (UserDeatails 实现类之一)是必定要用到 PasswordEncoderFactories.createDelegatingPasswordEncoder().encode() 加密是相应的。
认证的顶级管理员 AuthenticationManager 为咱们提供了 认证入口( authenticate()接口),可是呢,咱们也知道大老板通常不直接参与实质的工做,因此它把任务安排给它的下属,也就是咱们的 ProviderManager 部门领导 ,部门领导 肩负起 认证的工做(authenticate() 认证的实现),其实呢,咱们也知道部门领导也是 直接参数 认证工做的,它都是将实际任务安排给小组长的, 也就是咱们的 AuthrnticationProvider ,部门领导 开个会议,汇集了全部小组长 ,让它们自行判断(经过 support()) 大老板交下来的任务 该由谁来完成, 小组长 领到任务后,就把任务 分发给各个小组成员,好比 成员1(UserDetailsService) 只须要 完成 retrieveUser() 的工做,而后成员2 完成 additionalAuthenticationChecks() 的工做,最后由项目经理 ( createSuccessAuthentication() ) 将结果汇报给小组长,而后小组长汇报给部门领导,部门领导 审核一下结果,以为小组长作得不够好,而后又作了一些操做 ( eraseCredentials() 擦除密码信息 ),最后认为 结果 能够了就汇报给老板,老板呢,也很少看,直接将结果给了客户(filter)。
按照惯例,上流程图:
本文介绍认证过程的代码能够访问代码仓库中的 security 模块 ,项目的github 地址 : github.com/BUG9/spring…
若是您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!