Spring、Spring Boot 和 Spring Security 三者的关系如下图所示 :
一、spring security 的整体架构
二、核心组件
SecurityContextHolder,SecurityContext 和 Authentication
如图所示,用户在走完认证授权流程后,最终信息都将保存在SecurityContexHolder中, 存储当前应用程序安全上下文的详细信息,其中包括当前使用应用程序的主体的详细信息。如当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限等。
默认情况下,SecurityContextHolder
使用 ThreadLocal
来存储这些详细信息,这意味着 Security Context 始终可用于同一执行线程中的方法,即使 Security Context 未作为这些方法的参数显式传递。
- 获取当前用户的信息
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
- securityContext
上面代码中,SecurityContextHolder.getContext()中即为securityContext
// org/springframework/security/core/context/SecurityContext.java
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
其内部定义了身份验证的Authentication的get/set方法;
- 身份验证Authentication
使用getAuthentication时,会返回一个Authentication类型的对象,其内部包含的内容如下:
// org/springframework/security/core/Authentication.java
public interface Authentication extends Principal, Serializable {
// 权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
Collection<? extends GrantedAuthority> getAuthorities();
// 密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
Object getCredentials();
Object getDetails();
// 最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
1.权限信息列表
2.用户输入的密码串
3.UserDetails接口的实现类
4.身份验证结果
三、身份验证
常见流程
- 登录页提示用户输入用户名和密码
- 系统验证用户名和密码是否与注册的信息一致
- 验证后得到用户的信息、权限信息等
- 框架将信息保存至该用户上下文中
- 用户后续请求操作,根据权限信息进行安全验证
1~3步骤,身份验证流程、权限等信息如前面所说将保存至SecutiyContextHoder中。
- 通过
UsernamePasswordAuthenticationToken
获取用户请求中的用户名和密码,并做简单的非空检查;(由框架处理) - 令牌将传递给
AuthenticationManager
的实例以进行验证。 主要是使用username去数据源中获取用户密码,并交由AuthenticationManager
进行校验,校验部分由框架处理。 AuthenticationManager
在成功验证时返回完全填充的Authentication
实例。 (由框架处理)- SecurityContext 对象是通过调用
SecurityContextHolder.getContext().setAuthentication(…)
创建的,传入返回的身份验证 Authentication 对象。 (由框架处理)
四、核心服务
AuthenticationManager,ProviderManager 和 AuthenticationProvider
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,甚至,可能允许用户使用指纹登录,所以要求认证系统要支持多种认证方式。
Spring Security 中 AuthenticationManager 接口的默认实现是 ProviderManager,但它本身并不直接处理身份验证请求,它会委托给已配置的 AuthenticationProvider 列表,每个列表依次被查询以查看它是否可以执行身份验证。每个 Provider 验证程序将抛出异常或返回一个完全填充的 Authentication 对象。
也就是说,Spring Security 中核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登录则对应了三个 AuthenticationProvider。
ProviderManager 中维护了AuthenticationProvider列表, 在 ProviderManager 进行认证的过程中,会遍历 providers 列表,判断是否支持当前 authentication 对象的认证方式,若支持该认证方式时,就会调用所匹配 provider(AuthenticationProvider)对象的 authenticate 方法进行认证操作。若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException
异常。
下面将介绍常用的AuthenticationProvide。
DaoAuthenticaitonProvide
Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。 在实际项目中,最常见的认证方式是使用用户名和密码。用户在登录表单中提交了用户名和密码,而对于已注册的用户,在数据库中已保存了正确的用户名和密码,认证便是负责比对同一个用户名,提交的密码和数据库中所保存的密码是否相同便是了。
在 Spring Security 中,对于使用用户名和密码进行认证的场景,用户在登录表单中提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法就是 retrieveUser,虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作用,返回一个 UserDetails。retrieveUser 方法的具体实现如下:
// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/dao/DaoAuthenticationProvider.java
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);
}
}
在 DaoAuthenticationProvider 类的 retrieveUser 方法中,会以传入的 username 作为参数,调用 UserDetailsService 对象的 loadUserByUsername 方法加载用户。
Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是经过了 provider (AuthenticationProvider) 认证之后被填充的。
UserDetailsService 接口
大多数身份验证提供程序都利用了 UserDetails
和 UserDetailsService
接口。
在 UserDetailsService 接口中,只有一个 loadUserByUsername 方法,用于通过 username 来加载匹配的用户。当找不到 username 对应用户时,会抛出 UsernameNotFoundException 异常。UserDetailsService 和 AuthenticationProvider 两者的职责常常被人们搞混,记住一点即可,UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。
UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,当然你也可以自己实现 UserDetailsService。
来源:oschina
链接:https://my.oschina.net/zxh821215/blog/3209479