SpringSecurity-4-认证流程源码解析
登录认证基本原理
Spring Security的登录验证核心过滤链如图所示
请求阶段
-
SpringSecurity过滤器链始终贯穿一个上下文SecurityContext和一个Authentication对象(登录认证主体)。 -
只有请求主体通过某一个过滤器认证, Authentication
对象就会被填充,如果验证通过isAuthenticated=true -
如果请求通过了所有的过滤器,但是没有被认证,那么在最后有一个FilterSecurityInterceptor过滤器(名字看起来是拦截器,实际上是一个过滤器),来判断 Authentication
的认证状态,如果isAuthenticated=false(认证失败),则抛出认证异常。
响应阶段
-
响应阶段,如果FilterSecurityInterceptor抛出异常,则会被ExceptionTranslationFilter进行相应的处理,例如:用户名密码登录异常,然后被重新跳转到登录页面。 -
如果登录成功,请求响应会在SecurityContextPersistenceFilter过滤器中将返回的authentication的信息,如果有就放入session中,在下次请求的时候,就会直接从SecurityContextPersistenceFilter过滤器的session中获取认证信息,避免重复多次认证。
SpringSecurity多种登录认证方式
SpringSecurity使用Filter实现了多种登录认证方式,如下:
-
BasicAuthenticationFilter认证HttpBasic登录认证模式 -
UsernamePasswordAuthenticationFilter实现用户名密码登录认证 -
RememberMeAuthenticationFilter实现记住我功能 -
SocialAuthenticationFilter实现第三方社交登录认证,如微信,微博 -
Oauth2AuthenticationProcessingFilter实现Oauth2的鉴权方式
认证流程源码分析
认证流程图
如图所示,用户登录使用用户密码登录认证方式的(其他认证方式也可以)。UsernamePassword AuthenticationFilter会使用用户名和密码创建一个UsernamePasswordAuthenticationToken作为登录凭证,从而获取Authentication对象,Authentication代码身份验证主体,贯穿用户认证流程始终。
UsernamePasswordAuthenticationFilter
在UsernamePasswordAuthenticationFilter过滤器中用于获取Authentication
实体的方法是attemptAuthentication,其源码分析如下:
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
//请求方式要post
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " +
request.getMethod());
}
//从 request 中获取用户名、密码
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 将username和 password 构造成一个 UsernamePasswordAuthenticationToken 实例,
// 其中构建器中会是否认证设置为 authenticated=false
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
//向 authRequest 对象中设置详细属性值。如添加了 remoteAddress、sessionId 值
setDetails(request, authRequest);
//调用 AuthenticationManager 的实现类 ProviderManager 进行验证
return this.getAuthenticationManager().authenticate(authRequest);
}
多种认证方式的ProviderManager
AuthenticationManager接口是对登录认证主体进行authenticate认证的,源码如下
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
ProviderManager实现了AuthenticationManager的登录验证核心类,主要代码如下
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private List<AuthenticationProvider> providers = Collections.emptyList();
@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();
// 迭代认证提供者,不同认证方式有不同提供者,如:用户名密码认证提供者,手机短信认证提供者
for (AuthenticationProvider provider : getProviders()) {
// 选取当前认证方式对应的提供者
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
// 进行认证操作
// AbstractUserDetailsAuthenticationProvider》DaoAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
//认证通过的话,将认证结果的details赋值到当前认证对象authentication。然后跳出循环
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
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;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
}
请注意查看我的中文注释
AuthenticationProvider
认证是由 AuthenticationManager 来管理的,真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体,Spring Security 默认会使用 DaoAuthenticationProvider
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider的接口实现有多种,如图所示
-
RememberMeAuthenticationProvider定义了“记住我”功能的登录验证逻辑 -
DaoAuthenticationProvider加载数据库用户信息,进行用户密码的登录验证
DaoAuthenticationProvider
DaoAuthenticationProvider使用数据库加载用户信息 ,源码如下图
我们发现DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider;AbstractUserDetailsAuthenticationProvider是一个抽象类,是 AuthenticationProvider 的核心实现类,实现了DaoAuthenticationProvider类中的authenticate方法,代码如下
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvide的Authentication方法源码
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//如果authentication不是UsernamePasswordAuthenticationToken类型,则抛出异常
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//从缓存中获取UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
//当缓存中没有UserDetails,则从子类DaoAuthenticationProvider中获取
if (user == null) {
cacheWasUsed = false;
try {
//子类DaoAuthenticationProvider中实现获取用户信息,
// 就是调用对应UserDetailsService#loadUserByUsername
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
...
}
...
}
try {
//前置检查。DefaultPreAuthenticationChecks 检测帐户是否锁定,是否可用,是否过期
this.preAuthenticationChecks.check(user);
// 检查密码是否正确
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
// 异常则是重新认证
if (!cacheWasUsed) {
throw ex;
}
cacheWasUsed = false;
// 调用 loadUserByUsername 查询登录用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//后检查。由DefaultPostAuthenticationChecks实现(检测密码是否过期)
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {//是否放到缓存中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//将认证成功用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider从数据库获取用户信息
DaoAuthenticationProvider类中的retrieveUser方法
当我们需要使用数据库方式加载用户信息的时候,我么就需要实现UserDetailsService接口,重写loadUserByUsername方法
SecurityContext
登录认证完成以后,就需要Authtication信息,放入到SecurityContext中,后续就直接从SecurityContextFilter获取认证,避免重复多次认证。
注:注意查看我代码中的中文注释
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!
原文始发于微信公众号(springboot葵花宝典):SpringSecurity-4-认证流程源码解析
暂无评论内容