Spring Security 基于表单登录的源码分析
一.思维导图

二.原理分析
首先我们先来看一下 Spring Security 的表单认证的流程图:

从流程图中我们可以不难看出,整个认证流程大致上分为3个模块:
1.登录信息的封装
2.认证处理
3.结果处理(成功&失败处理)
其中最核心模块为认证模块,下面我们来看看认证模块 AuthenticationManager的相关类图:
该图可以分为两部分来看,分别是左边负责掌控全局的"大哥",以及右边勤勤恳恳的"小弟们"。"大哥" 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 用作密码的加密认证。
三.源码解析
在大致了解了它的原理之后,我们开始阅读源码;主要分两个模块来看,分别是:登录信息的封装以及认证处理。
1.登录信息的封装
登录信息的封装是指将前端传递的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。
2.认证处理
从上面我们也可以看到,在将登录信息封装成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基于表单登录的源码,包括登录信息封装、认证处理流程和具体实现。重点讲解了UsernamePasswordAuthenticationFilter如何封装登录信息,ProviderManager如何选择合适的认证器DaoAuthenticationProvider进行认证,以及DaoAuthenticationProvider如何从数据库获取用户并进行身份验证。
2万+

被折叠的 条评论
为什么被折叠?



