原理讲解
接上回分解,上回我们聊到了认证(一):基于表单登录的认证模式,正所谓:“知其然,然后知其所以然”;就让我们来一探究竟,瞅瞅 Spring Security
到底是怎么实现表单登录的。
首先我们先来回顾一下上篇文章我们制作的 Spring Security
的表单认证的流程图:
从流程图中我们可以简单的了解到,整个认证流程大致上分为3个模块:
-
登录信息的封装
-
认证
-
收尾处理(成功&失败处理)
核心模块为 认证模块
,下面就来看看认证模块 AuthenticationManager
的相关类图:
该图可以分2块来看,分别是左边负责掌控全局的"大哥",以及右边勤勤恳恳的"小弟们"。
总所周知,“大哥"都不需要亲历亲为的,所以"大哥” AuthenticationManager
认证管理接口,只定义了认证方法 authenticate()
,具体咋实现就让小弟们去搞吧~~
“二当家” ProviderManager
为认证管理类,实现了 AuthenticationManager
(二当家肯定要听大哥的话),并在认证方法 authenticate()
中将身份认证委托给具有认证资格的 AuthenticationProvider
(真正干活的小弟们);同时
ProviderManaer
有一个成员变量 List<AuthenticationProvider> providers
用以存储了所有具体执行认证的"小弟们"。
接下来介绍一下右边勤勤恳恳的"小弟们",首先是AuthenticationProvider
认证接口类,其定义了身份认证方法authenticate()
;这个也比较好理解;你怎么证明自己是我的"小弟"呢?当然是得入我门为我干活拉!AuthenticationProvider
接口就是起这个作用。
AbstractUserDetailAuthenticationProvider
为认证抽象类,实现了接口AuthenticationProvider
,同时还定义了抽象方法retrieveUser()
用于从数据库中获取用户信息,以及additionalAuthenticationChecks()
做身份认证;这块可能会犯迷糊,为啥子这个"小弟"还是个抽象类呢?不必慌张,其实只是为了一些功能的复用。
DaoAuthenticationProvider
认证类继承于AbstractUserDetailAuthenticationProvider
抽象认证类,实现了上面提到的2个抽象方法retrieveUser和additionalAuthenticationChecks
;并自定义了一些成员变量:private UserDetailsService userDetailsService;
用以用户信息查询,以及private PasswordEncoder passwordEncoder
用作密码的加密认证。
源码解析
在大致了解了原理之后,就开始了我们的阅读源码之旅拉;分两个模块来看,分别是:登录信息的封装
以及 认证
。
登录信息的封装
登录信息的封装是指将前端传递的username和password
封装成 UsernamePasswordAuthenticationToken
。
UsernamePasswordAuthenticationFilter.class的 attemptAuthentication()
方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 将http请求的Request带的认证参数:username、password转换为认证的token对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 设置一些详细信息, 诸如发送请求的ip等...
this.setDetails(request, authRequest);
// 调用AuthenticationManager的authenticate方法 执行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
attemptAuthentication()
方法做的事情很简单,主要是将登录信息 username和password
封装成 UsernamePasswordAuthenticationToken
。那么这个Token
到底是起什么作用呢?其实也很简单,主要是用于后续认证的时候,寻找匹配的认证处理器,例如表单登录的 UsernamePasswordAuthenticationToken
会唯一匹配相应的认证Provider
。
认证
从上面我们也可以看到,在将登录信息封装成Token
后,就调用了 AuthenticationManager
的 authenticate()
方法执行认证操作;因 AuthenticationManager
是一个接口,我们来分析它的实现类 ProviderManager
。
ProviderManager.class的authenticate()
方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
// 获取所有干活的“小弟” providers 认证器
Iterator var6 = this.getProviders().iterator();
// 挨个遍历,找到能支持当前登录方式(表单登录---由token来区分)的认证器
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
// 之前我们介绍过 AuthenticationProvider 接口,里面定义的supports方法,就是用于判定一个provider支持那种类型的认证方式
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
// 匹配到对应的provider后,调用provider的authenticate方法进行认证
try {
result = provider.authenticate(authentication);
if (result != null) {
// 认证成功,copy一些细节的参数到认证对象上
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var11) {
this.prepareException(var11, authentication);
throw var11;
} catch (InternalAuthenticationServiceException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (AuthenticationException var13) {
lastException = var13;
}
}
}
if (result == null && this.parent != null) {
try {
result = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var9) {
} catch (AuthenticationException var10) {
lastException = var10;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
this.eventPublisher.publishAuthenticationSuccess(result);
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{
toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
this.prepareException((AuthenticationException)lastException, authentication);
throw lastException;
}
}
ProviderManager
的 authenticate()
方法阅读起来也不困难,目的性十分的明确;首先是找到所有的认证器(干活的“小弟们”),挨个遍历根据Token
进行匹配,如果匹配成功则进行认证。因本文分析的是表单登录,所以根据UsernamePasswordAuthenticationToken
匹配到的 Provider
是 DaoAuthenticationProvider
。
DaoAuthenticationProvider.class的 authenticate()
方法 (PS: DaoAuthenticationProvider
继承于抽象类 AbstractUserDetailsAuthenticationProvider
,自身并无authenticate()
)
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 前置检查 该provider只支持 UsernamePasswordAuthenticationToken的认证方式
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
// 尝试从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
// 从缓存中获取不到用户信息, 调用子类 DaoAuthenticationProvider的retrieveUser方法,从数据库中加载用户信息
try {
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 {
// 预检查,之前我们介绍UserDetails的时候,有提到过几个方法,例如判断账号是否可用、账号是否过期等...
this.preAuthenticationChecks.check(user);
// 认证操作, 调用子类DaoAuthenticationProvider实现的additionalAuthenticationChecks进行认证
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
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();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
阅读代码我们可以看出,首先先尝试用缓存中获取用户,当从缓存中获取不到用户的时候,调用子类DaoAuthenticationProvider
实现的 retrieveUser()
方法,从数据库中加载用户信息,具体代码如下:
DaoAuthenticationProvider.class的 reretrieveUser()
方法
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 检查passwordEncoder
this.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");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
}
}
当加载完用户信息,进行预检查后,就调用子类DaoAuthenticationProvider.class的additionalAuthenticationChecks()
进行最终的认证校验
DaoAuthenticationProvider.class的additionalAuthenticationChecks()
方法
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 认证请求的密码非空判断
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
// 调用passwordEncoder的matches匹配方法,判断前端传递的密码和从数据库load出来的密码是否匹配
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
总结
总体来说,从登录信息的封装到最终的认证都比较的连贯;既然我们已经对Spring Security
的认证体系有了一定的了解,接下来我们也来尝试定制化开发自己的认证方式吧!
文章到这就结束拉,欢迎大家扫码关注小奇公众号~