最近在使用Spring Security的时候发现,在一些流程和架构上有些朦胧,决定复习一下,在这里记录下学习心得与理解,方便自己日后复习,也与大家共同探讨,这里是雪碧桑。
一个健壮的商业系统自然离不开优秀的认证和鉴权设计,这都是老生常谈的东西了。我们不着重讨论认证和鉴权的概念与具体方案,把重心放在SpringSecurity框架的架构原理以及使用上来。
!!!前排长文预警!!!
开始旅程
我们的旅程从一个简单的demo开始,它将展示SpringSecurity的易用性、健壮性。随后我们一步步深入源码,逐渐改进demo,在此过程中讨论Security的结构和原理。最后通过学习的知识实现SpringSecurity的最佳实践——JWT单点登录。
举个栗子
我们将使用SpringBoot + SpringSecurity来实现一个简单的demo程序。这个程序十分简单,它向外暴露一个名为为resource的Rest接口。用户要想访问resource接口,必须通过用户名+密码的认证。对,就这么简单!
编写demo
在这里我们使用Gradle作为demo程序的构建工具,如果不熟悉Gradle也没有关系,我们不会用到Gradle的高级特性。也可以使用熟悉的Maven来做构建工具。
-
使用IDEA 创建一个SpringBoot项目名为:security-demo
在依赖选择时别忘记选择web和security依赖!
在这里我还选择了
-
Lombok——在编译时帮助你生成Getter、Setter、构造方法等的小工具
-
Spring Configuration Processor——它能使你读取yml格式的配置文件,也正是我们要使用的SpringBoot配置方式。
点击Finish,我们的工程就建好了,只需等待Gradle引入依赖再刷新项目,我们就可以开始编码了。
-
-
编写我们的resource接口
创建一个controller包,在其中创建一个ResourceController类并编写代码:
@RestController @RequestMapping("resource") public class ResourceController { @GetMapping public String getResource(String name) { return "Hello world! Hello " + name + "!"; } }
可以看到我们的代码十分简单,它创建了一个Rest接口,指定url为resource、请求方式为Get、接收一个名为name的参数并返回一串带name的问候语。
-
完成了!一个需要用户名密码认证的resource接口!
测试demo
你可能会问,认证流程呢?密码验证代码呢?表单页面呢?这些Security都帮我们完成了,当我们的项目引入SpringSecurity依赖,它就会自动为我们保护所有现存的接口,并暴露一个login接口和一个简单的表单页面实现登录。当我们请求/logout时登出。用户名和密码?默认的用户名是user,密码每次启动项目时会随机生成并打印在控制台,我们现在就试试吧!
-
启动demo项目
-
在浏览器中输入localhost:8080/resource?name=sprite
此时你会发现浏览器重定向到一个登录页面:
这里就是Security为我们生成的简易登录页面,接下来我们使用它登录。
-
输入用户名user,在控制台找到密码粘贴到密码输入框,点击Sign in登录
如果一切顺利,你就请求到了resource接口,并看到了返回的数据:
-
请求/logout登出
当我们请求/logout,会看到登出的确认界面。
点击Log Out登出,再次请求resource接口就会重定向到login界面了。至此,我们的demo就完成了。你会发现我们并没有做多少工作,这就是Security强大的地方。它易用且健壮,在我们只是引入依赖的情况下,提供最基础的接口认证功能。
实现原理
我们暂时放下我们的demo开发,来看看Security是怎么做到如此简便的实现接口认证功能的。你会发现,在目前的情况下,我们甚至无法进行断点测试,断哪里好呢?所以在此之前,我们需要先了解一些基本概念。
Filter结构
我们在测试resource接口时发现,在没有登陆之前我们的请求被重定向到了登录页面,说明我们的请求被拦截下来了。那么在java web开发中哪里最适合拦截请求呢?答案就是Servlet Filter,还记得在没有这些安全框架时,我们是如何实现认证与鉴权的吗?不就是写一些Servlet Filter配置到Web容器拦截请求吗?Security也是这也做的,它自动配置了一个名为springSecurityFilterChain
的Filter,类型是FilterChainProxy
。从它的名称可以看出它只是一个代理类,并不是真正下地干活的人,我们来看看它的源码。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 在这里调用了doFilterInternal方法
doFilterInternal(request, response, chain);
}
catch (RequestRejectedException ex) {
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
}
finally {
// 请求处理结束后清理认证数据
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
接下来看看doFilterInternal
方法。
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
看起来非常乱不是吗?其实这个Filter就干了一件事:
如果filters
中有Filter就用VirtualFilterChain
遍历filters
执行,没有就让当前的FilterChain
继续执行。
VirtualFilterChain
遍历filters
执行完成后也会让当前的FilterChain
继续执行。
这里和SpringMVC代理容器中的Filters执行有着类似的实现方式,相当于在原本的Filter链条中加入了一个子链。
总结一下,SpringSecurity自己维护了一个Filter链,这个链条的入口和出口就是FilterChainProxy
,而实际做认证和鉴权操作的,是在这个链里面的Filter。
默认的Filter们
现在我们利用断点测试,看看默认情况下,Security维护了那些Filter
。
可以看到一共有15个Filter,还是非常多的,其中有一部分是固定做一些通用处理的,我们马上就会说明。
在这之前需要提及一个概念,
SecurityContext
——安全上下文。Security会把用户认证后的相关信息储存在SecurityContext
,它的存在贯穿整个请求,SecurityContext
由SecurityContextHolder
持有,SecurityContextHolder
会通过ThreadLocal
的方式储存SecurityContext
。这也就保证了在处理请求的任何地方都可以通过SecurityContextHolder.getContext()
获取到该请求对应的SecurityContext
。SecurityContext
默认是有的缓存机制的。在请求结束时,会把本次的SecurityContext
储存在HttpSession
中。下次同一个用户发起的请求时,从HttpSession
中获取SecurityContext
。
Security提供了三种
SecurityContextHolder
策略,当然最常用的也是默认的是ThreadLocal
的方式。
下面我们来看看SpringSecurity维护的这些Filter
- WebAsyncManagerIntegrationFilter:将
SecurityContext
集成到Spring异步任务中,保证我们使用Spring的异步任务时,可以正常拿到SecurityContext
中的用户信息,不是本文重点不做赘述。 - SecurityContextPersistenceFilter:请求来临时,尝试从
HttpSession
获取SecurityContext
,获取不到创建SecurityContext
,将SecurityContext
放入SecurityContextHolder
。请求结束时清空SecurityContextHolder
,将SecurityContext
放入HttpSession
。
值得一提的是,Security默认使用
HttpSessionSecurityContextRepository
,是使用HttpSession
进行SecurityContext
缓存,但是时下流行无状态服务,不启用Session
。之后会使用JWT标准解决这个问题。
-
HeaderWriterFilter:添加一些响应时的
Header
,具体取决于Security配置,不重要但必须有。 -
CsrfFilter:处理跨站请求伪造的
Filter
,在前后端分离的项目中设置不启用,实现方式不做赘述。 -
LogoutFilter:默认的登出操作
Filter
,匹配/logout请求地址,清理Session
。 -
UsernamePasswordAuthenticationFilter:重中之重!是我们之后的主要分析的案例!用于处理用户名和密码认证。
-
DefaultLoginPageGeneratingFilter:默认的登录页面生成。
-
DefaultLogoutPageGeneratingFilter:默认的登出页面生成。
-
BasicAuthenticationFilter:处理Basic标准下的认证,Basic是一种认证方案的标准。
-
RequestCacheAwareFilter:做请求缓存命中的
Filter
,发现有缓存的请求时,跳转到指定的请求。在我们登录成功之后,会自动跳转到登陆前的请求,默认情况下使用HttpSession
。它与ExceptionTranslationFilter
协同工作,ExceptionTranslationFilter
详情看后文。 -
SecurityContextHolderAwareRequestFilter:对
Request
进行包装,添加一些获取认证信息的方法。 -
AnonymousAuthenticationFilter:如果之前的
Filter
都没有处理认证的话,这个Filter
会给予请求一个匿名认证。 -
SessionManagementFilter:提供
Session
的固化保护和Session
的并发控制,就是保证Session
不会永久停留在系统中,控制Session
同时存在的数量。后续会配置不启用这个Filter
,因为JWT是无状态的。 -
ExceptionTranslationFilter:这也是
Fliter
中的一个重点,它负责拦截两种异常:AuthenticationException
和AccessDeniedException
。AuthenticationException
会在认证不通过时抛出,AccessDeniedException
会在鉴权不通过时抛出。这两种异常我们会在之后分析UsernamePasswordAuthenticationFilter
时详细讲解,在这里简单了解下即可。 -
FilterSecurityInterceptor:最终干活的拦截器了,这里就是具体判断这个请求有没有认证,认证过的话有没有权限来访问指定的URL。它会根据你对Security的配置,拦截指定的请求并进行认证和鉴权的比对。
比如我们的demo项目,Security默认匹配全路径,所以我们没有登录时,resource接口最终会被
FilterSecurityInterceptor
拦截下来,进行认证判断后发现用户未登录,抛出AuthenticationException
被ExceptionTranslationFilter
捕获并处理,处理结果就是重定向到登录页面并缓存我们原本的请求,方便在登录成功后跳转。
demo分析
是不是到此为止还看的一头雾水?不必太在意那些默认Filter
的具体细节,了解一下结构和作用即可。接下来我们将逐步分析在我们请求resource接口的时候,Security帮我们做了什么。
这里引入另一个概念——Authentication
认证结果,也就是请求主体信息。我们都知道,认证意味着知道发出请求的主体是谁。Security在实现认证时,就是请求穿过一系列的Filter
,如果其中一个Filter
恰好可以认证当前请求主题是谁,那就认证成功了。具体到处理认证的Filter
就是拿到一个请求先去判断自己是否可以认证,如果可以,认证成功就储存认证结果到SecurityContext
,失败就抛出异常。之后的认证Filter
检查SecurityConetxt
发现已经经过认证了,就不再重复认证。我们可以通过Authentication
获取一些用户的信息,比如用户名、角色、权限等等。
想象一种情况,我们没有登录系统,直接请求resource接口,这个过程中谁发挥了主要作用呢?
当我们在没有登录的情况下直接请求resource,请求一路穿过上面的层层Filter
,到达AnonymousAuthenticationFilter
,AnonymousAuthenticationFilter
检查SecurityContextHolder
发现这个请求没有被前面的任何一个Filter认证,给予这个请求一个匿名认证。还记得吗?Security的默认配置下,所有的接口都是被认证保护的,即需要且只需要登录就可以请求所有的接口。所以请求到达最后一个FilterSecurityInterceptor
被拦截下来进行认证比对,FilterSecurityInterceptor
发现该请求持有匿名身份未认证,抛出AuthenticationException
。AuthenticationException
将会被ExceptionTranslationFilter
捕获并处理,ExceptionTranslationFilter
将AuthenticationException
交给默认的authenticationEntryPoint
处理并缓存这次请求。默认的authenticationEntryPoint
会将请求重定向到登录页面。所以你就看到了登录页面,而且你原本的请求也被缓存下来等待你登录成功后跳转。
接下来我们执行登录操作,看看Security会如何处理。
当我们输入用户名和密码点击登录,我们的请求以此穿过上面的Filter
,到达UsernamePasswordAuthenticationFilter
。这个Filter只匹配/login的Post请求,其他的一律放行。之前提到这个Filter
是重中之重。
下面是一波源码解析,这边建议你先去室外吹吹风缓缓劲(:。
首先看看UsernamePasswordAuthenticationFilter
的声明:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
可以看到它继承自AbstractAuthenticationProcessingFilter
,doFilter
方法是由AbstractAuthenticationProcessingFilter
实现的:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 不匹配/login URL直接跳过这个Filter
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 这里调用了attemptAuthentication
// attemptAuthentication是由子类实现的
// Authentication 是认证成功之后,获取到的用户信息
// 如果认证成功就返回Authentication,没成功就抛出异常
// 如果既没成功也没失败就返回 null 交给之后的Filter去认证
Authentication authenticationResult = attemptAuthentication(request, response);
// 处理null的情况
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 通过不同的Session策略,比如删除Csrf token,更换SessionID
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
// 默认是false,大部分情况也不会修改
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 验证成功
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
// 如果子类的处理抛出AuthenticationException 异常,执行认证失败的方法
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
再来看看验证成功后的successfulAuthentication
方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 将认证信息存储在SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
// 打印日志
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 处理记住我功能
this.rememberMeServices.loginSuccess(request, response, authResult);
// 发布一个Spring事件
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// !!! 注意这里交给successHandler进行处理了
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
默认的successHandler
是SavedRequestAwareAuthenticationSuccessHandler
,它会使请求重定向到登录之前请求的接口。
这里重定向的接口,也就是
ExceptionTranslationFilter
处理AuthenticationException
时缓存的请求。如果你还记得RequestCacheAwareFilter
的话,你就大概能猜到在默认情况下RequestCacheAwareFilter
什么都不用做,因为登录成功后直接由successHandler
转发到了缓存的请求。
然后是失败后处理:
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
和成功时一样,交给failureHandler
进行处理,默认情况下就是返回401。
总结一下,AbstractAuthenticationProcessingFilter
把最重要的认证交给子类去实现,自己只做了认证成功之后的处理。根据情况最终交给successHandler
或failureHandler
去做后续处理。
下面我们来看看UsernamePasswordAuthenticationFilter
实现的attemptAuthentication
方法:
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());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 将用户名和密码组装成UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 将认证移交给getAuthenticationManager处理
return this.getAuthenticationManager().authenticate(authRequest);
}
我在写文章的同时一边分析代码,感觉到这里开始难度陡增,希望大家可以看懂我写的拙文。
读源码的过程真的是痛苦但收获颇丰,可以说是看到了世界的参差。
其实本质上UsernamePasswordAuthenticationToken
就是一个未认证的Authentication
:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer
可以看到最终的认证交给了AuthenticationManager
,AuthenticationManager
是一个接口,其中只有一个方法,就是对指定的Authentication
进行认证,返回认证后的Authentication
:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
但这里获取到的AuthenticationManager
只是一个代理,是AuthenticationManager
的孙子类ProviderManager
,ProviderManager
负责为UsernamePasswordAuthenticationToken
找到合适的AuthenticationManager
处理认证。ProviderManager
在程序中的结构如下:
每个ProviderManager
又维护了一个AuthenticationProvider
集合,AuthenticationProvider
和AuthenticationManager
差不多,也是处理认证,只是多了一个support
方法,检查它是否支持处理某种Authentication
的认证:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
我们来看看ProviderManager
的核心代码(删除了日志打印):
// 遍历AuthenticationProvider集合
for (AuthenticationProvider provider : getProviders()) {
// 检查AuthenticationProvider是否可以处理
// 不能处理就跳过
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
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;
}
}
// 如果最后还是没能找到合适的AuthenticationProvider,且有parent
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
// 传递到parent执行
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;
}
}
当UsernamePasswordAuthenticationFilter
把认证移交给ProviderManager
处理时,会从最下面的ProviderManager
开始遍历调用其中AuthenticationProvider
集合的supports
方法,确认AuthenticationProvider
能否认证UsernamePasswordAuthenticationToken
。如果当前ProviderManager
中的AuthenticationProvider
集合不能处理UsernamePasswordAuthenticationToken
,将会继续在parent
中寻找AuthenticationProvider
,一直找到最顶上的ProviderManager
还不能处理就返回Null,交给其他认证Filter
处理。
遍历顺序大概如下:
真实画技,假一赔十
幸运的是,我们的UsernamePasswordAuthenticationToken
是有人处理的,在遍历到第二个ProviderManager
时找到了它的归宿——DaoAuthenticationProvider
。我们来看一下DaoAuthenticationProvider
的声明:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
以及它的父类声明:
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware
可以看到DaoAuthenticationProvider
是间接实现了AuthenticationProvider
,authenticate
方法实现在AbstractUserDetailsAuthenticationProvider
中:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 断言,只能处理UsernamePasswordAuthenticationToken的Authentication
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));
// 通过用户名尝试读取缓存中的用户信息
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
// 缓存中读取不到
if (user == null) {
cacheWasUsed = false;
try {
// 调用需要子类实现的方法来读取用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 预先检查
this.preAuthenticationChecks.check(user);
// 调用需要子类实现的认证检查
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 如果使用了缓存中的数据认证失败,再用获取的数据重试一次(严谨
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// 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);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 滞后检查
this.postAuthenticationChecks.check(user);
// 更新缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
其实最重要的两方法是retrieveUser
和additionalAuthenticationChecks
,这两个方法都是子类来实现的,我们来看看DaoAuthenticationProvider
都做了什么:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 直接从UserDetailsService中读取用户信息
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);
}
}
其实看到这里,有些拥有Security使用经验的人大都知道,UserDetailsService
大部分情况都是由我们自己来提供的。目前系统默认的UserDetailsService
会返回固定的用户名user+随机密码的UserDetails
。
再来看看认证检查:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 检查用户输入的密码是否为null
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
// 获取用户输入的密码
String presentedPassword = authentication.getCredentials().toString();
// 使用passwordEncoder来对两个密码进行比对,比对不成功抛出异常
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
同样的,拥有Security使用经验的人大都知道,passwordEncoder
大部分情况下也是我们提供给Security,其中封装了密码的编码和比对算法。默认情况下Securiy使用NoOpPasswordEncoder
,就是密码不编码,直接字符串比对。当然Security还贴心的为我们准备了各种开箱即用的PasswordEncoder
,比如比较常用的BCryptPasswordEncoder
,它使用BCrypt算法对密码进行编码和比对。BCrypt算法的细节就不多赘述了。
写到这里,我们的认证流程算是走到最低层了,密码比对成功无异常,层层返回,执行AbstractUserDetailsAuthenticationProvider
的
return createSuccessAuthentication(principalToReturn, authentication, user);
再从ProviderManager
中层层返回到UsernamePasswordAuthenticationFilter
,一直到UsernamePasswordAuthenticationFilter
的父类AbstractAuthenticationProcessingFilter
,执行其中的successfulAuthentication
方法把认证结果储存在SecurityContext
中,再把后续操作交给successHandler
处理(默认是跳转到登陆之前请求的接口)。
这样整个用户名密码认证的流程就结束了。请求结束后,Security的第二个Filter
——SecurityContextPersistenceFilter
会帮我们把SecurityConetxt
保存在Session
中,下次用户认证直接通过Session
,登出操作也只是简单的清空Session
。
读到这里,我想你应该了解Security最核心的认证流程。原本是想一气呵成直接写完的,结果不知不觉洋洋洒洒写了这么多了,我想还是分两篇吧,下篇我们再讨论JWT单点登录吧。
雪碧自己也是职场小白,刚刚进入行业没多久,本着大家一起学习的心态写下这篇文章。如果文章中有疏漏和错误还请大家不要吝啬批评。