之前看过很多个版本的
Spring Security
中获取用户信息并进行密码校验,有在相关的Filter中获取的,也有在默认的
DaoAuthenticationProvider
中判断
Authentication
类型进行判断以后直接获取的。
不过,这些方式都不太“正宗”,
Spring Security
有自己的一套流程。至于此流程的详细信息讲解,先不急,本篇文章来认识一下其中比较重要的一环:获取用户信息并进行密码验证,即
DaoAuthenticationProvider
。
AuthenticationProvider
先来认识一下其实现的接口
AuthenticationProvider
,主要用于解析特定
Authentication
。
* Indicates a class can process a specific
* {@link org.springframework.security.core.Authentication} implementation.
其中,有两个接口方法。
首先是
authenticate(Authentication authentication)
方法。
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
该方法与
AuthenticationManager
中的
authenticate
声明及功能完全一致,返回包含凭据的完整身份验证对象
authentication
。但是,如果
AuthenticationProvider
不支持给定的
Authentication
的话,该方法可能会返回
null
。在此情况下,下一个支持
authentication
的
AuthenticationProvider
将会被尝试(关于此段逻辑,
AuthenticationManager
中会有详细的逻辑代码及说明,后续再详细讲解)。
其次是
supports(Class<?> authentication)
方法。
boolean supports(Class<?> authentication);
如果
AuthenticationProvider
支持给定的
Authentication
的话,会返回
true
。但是,这并不保证
AuthenticationProvider
能够对给定的
Authentication
进行身份认证,它只是表明它可以支持对其进行更深入的评估,
AuthenticationProvider
依然可以返回
null
,以指示应尝试另一个
AuthenticationProvider
。
此方法是用以选择一个能够匹配
Authentication
以胜任身份认证工作的
AuthenticationProvider
,交给
ProviderManager
来执行。
AbstractUserDetailsAuthenticationProvider
用于解析
UsernamePasswordAuthenticationToken
以进行身份认证的基础
AuthenticationProvider
。其子类可以重写或使用其
UserDetails
对象。
其实现了
AuthenticationProvider
接口的
authenticate(Authentication authentication)
方法,其认证过程可分为一下几个步骤。
获取用户名。
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
缓存中获取 User。
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
如果缓存中获取的 User 为 null,再调用
retrieveUser
(子类实现)方法获取。
if (user == null) {
cacheWasUsed = false;
try {
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;
......
如果找不到用户,会抛出 UsernameNotFoundException 异常。此时,处理方案有2个。
前置身份认证检查。
preAuthenticationChecks.check(user);
默认的前置身份认证检查逻辑如下,即事先校验一下用户的账号是否锁定、是否可用、是否过期。
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"));
额外身份认证校验,对于 UsernamePasswordAuthenticationToken
来说就是凭据/密码校验。
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;
如果认证过程中发生异常,会有如下处理逻辑:
-
如果当前的用户是从用户缓存中取出的,则使用原有的用户信息再进行一次身份认证,即获取用户信息、前置身份认证检查、额外身份认证检查;
-
如果不是从用户缓存中取出的,则直接抛出异常。
具体的校验逻辑在下面的 DaoAuthenticationProvider 中会进行详细说明,此处暂且不谈。
后置身份认证检查。
postAuthenticationChecks.check(user);
默认的后置身份认证检查逻辑如下,即检查一下用户的凭据/密码是否过期。
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"));
将当前用户放入用户缓存,如果当前用户还没有被用户缓存缓存的话。
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
转换 principal 为字符串类型。不过,需要 forcePrincipalAsString
参数为 true
(默认为 false
)。
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
最后,创建身份认证成功的 Authentication
。
return createSuccessAuthentication(principalToReturn, authentication, user);
默认的创建身份认证成功的 Authentication
逻辑如下。
return createSuccessAuthentication(principalToReturn, authentication, user);
即创建了一个新的 UsernamePasswordAuthenticationToken,与未认证的区别就是 principal 变成了检索到的用户详细信息(或者用户名,强制字符串principal)。
当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication
中存储用户提供的原始凭据/密码,而非加盐、加密过的。
AbstractUserDetailsAuthenticationProvider 的主要功能就是这些,剩下的,就是诸如 retrieveUser 、additionalAuthenticationChecks 等需要子类实现的抽象方法了,这就是属于子类 DaoAuthenticationProvider 的部分了。
DaoAuthenticationProvider
说到 Authentication
相信都不陌生。最著名的,便是 UsernamePasswordAuthenticationToken
。而 DaoAuthenticationProvider 便是用于解析并认证 UsernamePasswordAuthenticationToken
的这样一个认证服务提供者。
类注释说的也很明白。
* An {@link AuthenticationProvider} implementation that retrieves user details from a
* {@link UserDetailsService}.
其最终目的,就是根据 UsernamePasswordAuthenticationToken
,获取到 username
,然后调用 UserDetailsService
检索用户详细信息。
在其基类 AbstractUserDetailsAuthenticationProvider 中,我们已经讲过,需要子类实现 retrieveUser 、additionalAuthenticationChecks 等抽象方法。那么,我们就来详细看一下吧。
额外的身份认证检查,也即 additionalAuthenticationChecks,密码检查。
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"));
这里的检查逻辑比较简单,就不再赘述了。
接下来就是用户检索,即 retrieveUser。
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);
正如其类注释所述,需要调用 UserDetailsService
检索用户详细信息,如权限列表、存储密码等,逻辑也比较简单。但是,有两个特殊方法需要注意一下,即检索用户前调用的 prepareTimingAttackProtection()
方法,和抛出 UsernameNotFoundException
异常后调用的 mitigateAgainstTimingAttack(authentication)
方法。
这两个方法都是用于定时攻击保护的。
首先是 prepareTimingAttackProtection()
方法。
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
该方法从方法命名上理解,是为了准备定时攻击保护用的。其实,就是将 userNotFoundEncodedPassword
,也即用户未找到时的加密密码给准备好,然后使用配置的 passwordEncoder
来加密为密文,默认的用户未找到时的明文密码为 userNotFoundPassword。
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
其次是 mitigateAgainstTimingAttack(authentication)
方法。
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
逻辑也不复杂,就是在用户提供的凭据/密码不为空时,使用配置的 passwordEncoder
来验证两者是否相等。看到这里也许会有疑惑,这两者相等的概率太小了吧?一个是用户随便填的,一个是系统配置的,且无论相同与否,都不影响下面 UsernameNotFoundException
异常的抛出,除非我们在自定义的 passwordEncoder
中抛出另外一个非 UsernameNotFoundException
异常。
这里我们看一下方法命名中的 mitigate,意味缓解、减轻、缓和。
什么意思呢?我们需要从整体上想象一下。
用户登录时,调用 AuthenticationProvider
的 authenticate
方法,然后,先从用户缓存中获取用户,如果取不到,再调用 retrieveUser
方法检索用户。最后,再调用 additionalAuthenticationChecks
方法进行密码检查。既然定性为有目的的定时攻击,那么在发生此种情况后,就没有必要继续验证密码了,进一步也没有必要进行后续的身份认证处理了。
至于是否在自定义的 passwordEncoder
中抛出另外一个非 UsernameNotFoundException
异常,还是存疑的,感觉实在没有必要,这里倾向于直接返回密码检查结果,至于 UsernameNotFoundException
异常,在基类调用retrieveUser
方法是已有相关异常捕获逻辑,且可以自行配置是否隐藏该异常类型,所以,感觉没有必要。
这里也确实猜不透设计团队的意图,存个疑吧。有了解此番意图的,可联系作者说明,不胜感激!
另外,DaoAuthenticationProvider 还重写了基类的 createSuccessAuthentication 方法。
@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);
还记得我们在介绍基类的 createSuccessAuthentication 方法时所说的逻辑吗?
当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。
该方法便是对此段逻辑描述最好的呈现了。
至于 userDetailsPasswordService
,其实也很简单,就是更新一下 User 中的密码,仅此而已。
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
mutableUser.setPassword(newPassword);
return mutableUser;
自定义Provider
还记得前一篇文章中自定义的适用于CA登录认证的 CertificateAuthorityAuthenticationToken 和 CertificateAuthorityAuthenticationFilter 吗?
相对应的,这里我们就自定义个适用于CA登录认证的 CertificateAuthorityAuthenticationProvider。
public class CertificateAuthorityDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
public CertificateAuthorityDaoAuthenticationProvider() {
@Override
public boolean supports(Class<?> authentication) {
return (CertificateAuthorityAuthenticationToken.class.isAssignableFrom(authentication));
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// do nothing.
......
由于CA登录时,其CA KEY的密码是有前端配合控制器校验的,况且其密码也不在业务数据库中存储,不需要Spring Security框架处理,所以,无需进行额外的身份认证检查。
另外,需要修改一下 supports
方法,仅支持 CertificateAuthorityAuthenticationToken
类型的 Authentication
哦。
其它详细源码,请参考文末源码链接,可自行下载后阅读。
github
https://github.com/liuminglei/SpringSecurityLearning/tree/master/26
gitee
https://gitee.com/xbd521/SpringSecurityLearning/tree/master/26
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的
成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方
正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控
制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权
(Authorization)两个部分,这两点也是 Spring
文章目录DaoAuthenticationProviderDeclaredJdocretrieveUserDeclaredMethod CodeadditionalAuthenticationChecksDeclaredMethod CodecreateSuccessAuthenticationDeclaredMethod Code
DaoAuthenticationProvider
Declar...
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new LoginAuthenticationProvider(loginService));
`Spring Security`作为一个功能完善的安全框架,具有以下特性:
- **认证(Authentication)**:解决 "你是谁" 的问题,验证系统中是否有这个“用户”(用户/设备/系统),也就是我们常说的“登录”。
- **授权(Authorization)**:权限控制/鉴别,解决的是系统中某个用户能够访问哪些资源,即“你能干什么”的问题。`Spring Security` 支持基于 `URL` 的请求授权、方法访问授权、对象访问授权。
正文有三种情况我们需要配置多个AuthenticationProvider,甚至是自定义AuthenticationProvider:
1. 用户认证信息存储在多个地方
2. 在同一个地方但是需要多种授权方式,比如说手机号+密码可以登录,邮箱+密码也可以登录
3. 以上两种情况都有
配置AuthenticationProvider,可以通过AuthenticationManagerBuild
AbstractUserDetailsAuthenticationProvider类位于org.springframework.security.authentication.dao包下,在
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter中被调用,
该类的public Au...
开发遇到一件很尴尬的事情,springboot里面使用security做登录验证,但是默认的就是验证username和password,现在的第三方或者短信登录非常流行。我现在的需求是做账号+短信登录(ps:后面还不知道要加啥… so:旧的不能废,只能扩展),接rongcloud sdk
post:/login
params:{
username:mobile,
sessionid:rongclo...