Security是基于过滤器链实现认证授权的,它支持不同的认证机制,这里我们用户名密码认证机制。
Spring Security 提供了以下内置机制来从 读取用户名和密码:
- Form Login
- Basic Authentication
- Digest Authentication
用户名密码存储机制:
- 具有内存中身份验证的简单存储
- 具有JDBC 身份验证的关系数据库
- 使用UserDetailsService自定义数据存储
- 带有LDAP 身份验证的LDAP 存储
一、认证流程图
图来自网络
分析认证过程:
- 当用户提交他们的用户名和密码,则UsernamePasswordAuthenticationFilter创建一个UsernamePasswordAuthenticationToken其是一种类型的Authentication通过提取从所述用户名和密码HttpServletRequest。
- 过滤器将UsernamePasswordAuthenticationToken提交至认证管理器(AuthenticationManager)进行认证
- 如果身份验证失败,则失败,该SecurityContextHolder将被清除出去。RememberMeServices.loginFail被调用。如果记住我没有配置,这是一个空操作。最终AuthenticationFailureHandler 被调用。
- 如果认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。该认证被设置在SecurityContextHolder中。RememberMeServices.loginSuccess被调用。如果记住我没有配置,这是一个空操作。ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent。在AuthenticationSuccessHandler被调用。
二、源码分析
1、查看 UsernamePasswordAuthenticationFilter 过滤器
UsernamePasswordAuthenticationFilter 过滤器,主要用于认证操作,默认匹配 URL为 /login且必须为POST请求。
看到源码,我们应该明白,为什么默认是 post请求,url为/login,并且用户名和密码的参数名称为它。
在 attemptAuthentication方法中,主要做了两件事:
- 将填写的用户名和密码封装到了UsernamePasswordAuthenticationToken中
- 调用AuthenticationManager对象实现认证
1.1 用户名和密码封装到了UsernamePasswordAuthenticationToken中
将填写的用户名和密码封装到了 UsernamePasswordAuthenticationToken中
。
1.2 调用AuthenticationManager对象实现认证
由源码得知,真正认证逻辑是在 AuthenticationManager接口的 authenticate方法。接着看AuthenticationManager的实现类 ProviderManager类
的 authenticate方法。
2、查看 ProviderManager类
源码如下:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取传入的Authentication 类型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// 循环AuthenticationProvider对象,Spring Security针对每一种认证,比如第三方登录,用户名密码登录等,都封装了一个 AuthenticationProvider对象。
for (AuthenticationProvider provider : getProviders()) {
// 1.判断是否支持当前认证方式
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
// 2.调用找到支持的 AuthenticationProvider对象进行认证逻辑
result = provider.authenticate(authentication);
if (result != null) {
// 3.执行 authentication details 的拷贝逻辑
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException | InternalAuthenticationServiceException ex) {
// 4.如果发生 AccountStatusException 或 InternalAuthenticationServiceException 异常,则会通过 Spring事件发布器AuthenticationEventPublisher 发布异常事件。
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
} catch (AuthenticationException ex) {
lastException = ex;
}
}
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;
}
2.1 查看 AbstractUserDetailsAuthenticationProvider类
AbstractUserDetailsAuthenticationProvider是针对表单用户名密码登录进行认证的 AuthenticationProvider对象。
源码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
//1.获取用户名,此时封装在UsernamePasswordAuthenticationToken中
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//2.获取用户信息,我们自己的用户实现了 UserDetails接口
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
//3.账号状态和密码校验
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//4.又封装了一次UsernamePasswordAuthenticationToken,返回 Authentication
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
2.1.1 获取用户对象(UserDetails)
查看 retrieveUser方法。
- UserDetails对象是 Spring Security内部的认证用户对象。我们自定义的用户可实现 UserDetails接口。
- 我们的SysUserService继承了 UserDetailsService类,并重新了 loadUserByUsername方法。
3.1.2 账号状态和密码校验
1)账号状态校验
2)加密校验
使用 PasswordEncoder 密码解析器
对密码进行加密及解析。
BCryptPasswordEncoder
是SpringSecurity中最常用的密码解析器。
它使用BCrypt算法。特点是加密使用动态加盐sault,但是解密不需要盐。因为盐就在密文当中。这样可以通过每次添加不同的盐,而给同样的字符串加密出不同的密文。
密文比如:$2a 10 10 10lLi6A4xdn7lCWYgU5yIXoek9DeYC91mTf6d9nO4UUyB/Bv4QXq6.i
其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了。
2.1.3 又封装 UsernamePasswordAuthenticationToken对象
查看 createSuccessAuthentication方法。
这次又封装 UsernamePasswordAuthenticationToken对象,具体看 它的构造方法。
3、查看 UsernamePasswordAuthenticationToken类
源码如下:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
private final Object principal;
private Object credentials;
//认证成功前,调用的是这个带有两个参数的。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//认证成功后,调用的是这个带有三个参数的。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
//重点看父类
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // 标记已认证
}
}
3.1 查看 super(authorities)
查看父类 AbstractAuthenticationToken的 构造方法,源码如下:
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {
private final Collection<GrantedAuthority> authorities; // 用户的权限信息集合
private Object details;
private boolean authenticated = false;
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
if (authorities == null) {//这是两个参数
this.authorities = AuthorityUtils.NO_AUTHORITIES;
} else {//三个参数的,看这里
Iterator var2 = authorities.iterator();
//1.添加用户的权限信息
GrantedAuthority a;
do {
if (!var2.hasNext()) {
ArrayList<GrantedAuthority> temp = new ArrayList(authorities.size());
// 添加权限,GrantedAuthority类型的
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
return;
}
a = (GrantedAuthority)var2.next();
} while(a != null);
//2.若没有权限信息,是会抛出异常
throw new IllegalArgumentException("Authorities collection cannot contain any null elements");
}
}
}
这里主要是添加用户的权限信息,因为,我们的 角色类实现了 GrantedAuthority接口。获取用户信息是,也已经将 角色信息放到了 用户信息中,所以这里也不难看懂。
所以,我们自定义认证业务逻辑返回的 UserDetails对象中一定要放置权限信息(GrantedAuthority类型的)。
到此,认证流程中的用户信息封装认证完毕,接下来看具体的 认证成功之后的逻辑。
4、查看 doFilter方法
回到最初的地方 UsernamePasswordAuthenticationFilter 过滤器,我们没有找到 doFilter方法,本类没有,那就去父类找!
查看父类 AbstractAuthenticationProcessingFilter的 doFilter方法
,源码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
//1. 判断是否需要认证
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//2.调用子类方法获取认证信息,封装到Authentication中
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 3. Session 策略处理
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
//5.认证失败处理
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 4. 认证成功处理
this.successfulAuthentication(request, response, chain, authResult);
}
}
调用子类方法获取认证信息,上面我们已经分析了。
4.1 查看 successfulAuthentication方法
//认证成功,调用 successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder tocontain: " + authResult);
}
//1.认证成功,将认证信息存储到SecurityContext中!
SecurityContextHolder.getContext().setAuthentication(authResult);
//2.登录成功调用rememberMeServices
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new
InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
4.2 查看 unsuccessfulAuthentication方法
//认证失败,调用unsuccessfulAuthentication
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " +
this.failureHandler);
}
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
上面就是认证流程的大致过程。
– -- 求知若饥,虚心若愚。