源码分析:Spring Security 表单登录(上)源码分析:Spring Security 表单登录(上) 0、前
源码分析:Spring Security 表单登录(上)
0、前言
Spring Security 提供了对通过 HTML 表单提供用户名和密码的支持。本节将详细介绍基于表单的认证在 Spring Security 中如何工作。
本节研究了基于表单的登录在 Spring Security 中是如何工作的。在上半节中,我们解析用户是如何被重定向到登录表单的。在下半节中,我们完成剩余部分的解析,即从用户登录认证开始的剩余认证流程。
- 客户端发出一个未经认证的请求,向服务器请求一个未被授权的资源
/private
。 - Spring Security 的
FilterSecurityInterceptor
抛出一个AccessDeniedException
来表明未经认证的请求被拒绝了。 - 由于用户没有被认证,
ExceptionTranslationFilter
捕获了AccessDeniedException
,并通过LoginUrlAuthenticationEntryPoint
告诉客户端重定向到/login
。 - 客户端发送
GET /login
请求。 - 服务器返回
login.html
。
下边从 FilterChainProxy
开始,解释代码执行流程。
1、FilterChainProxy
中的执行流程
1)所有过滤器的核心方法都是 doFilter(...)
,FilterChainProxy
的 doFilter(...)
方法的核心代码是 doFilterInternal(...)
方法。
/**
* 过滤器的核心方法,负责处理每个请求并确保安全上下文的清理和异常处理。
* 这个方法会被调用以处理 HTTP 请求,处理请求前会进行状态检查,确保过滤器的处理逻辑只执行一次。
*
* @param request 请求对象,代表客户端的 HTTP 请求。
* @param response 响应对象,代表服务器向客户端返回的 HTTP 响应。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果在处理请求时发生 I/O 错误,则抛出此异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,则抛出此异常。
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断当前请求是否已经通过此过滤器处理(检查 FILTER_APPLIED 属性)
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
// 如果请求已经被处理过,则直接调用内部处理方法,跳过后续逻辑
if (!clearContext) {
doFilterInternal(request, response, chain);
return; // 结束当前处理
}
// 否则,设置 FILTER_APPLIED 属性,标记该请求已处理
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 调用内部的过滤方法进行处理
doFilterInternal(request, response, chain);
}
catch (Exception ex) {
// 处理异常链,分析异常的根本原因
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
// 获取异常链中的第一个 RequestRejectedException 异常
Throwable requestRejectedException = this.throwableAnalyzer
.getFirstThrowableOfType(RequestRejectedException.class, causeChain);
// 如果捕获到的异常不是 RequestRejectedException,重新抛出原异常
if (!(requestRejectedException instanceof RequestRejectedException)) {
throw ex;
}
// 如果是 RequestRejectedException,则调用自定义的处理方法
// 处理特定的请求拒绝异常
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response,
(RequestRejectedException) requestRejectedException);
}
finally {
// 确保无论如何都会清理 SecurityContextHolder,防止泄漏安全上下文信息
SecurityContextHolder.clearContext();
// 移除 FILTER_APPLIED 属性,允许下一个请求重新通过过滤器
request.removeAttribute(FILTER_APPLIED);
}
}
2)FilterChainProxy
的 doFilterInternal
方法。核心逻辑是创建一个虚拟过滤器链 VirtualFilterChain
,按顺序执行所有配置的过滤器。
下一步进入 VirtualFilterChain
的 doFilter
方法。
/**
* 过滤器的内部方法,负责对请求应用防火墙规则并执行安全过滤。
* 如果没有配置安全过滤器,则直接将请求传递给后续处理链。
* 如果配置了过滤器,则使用 `VirtualFilterChain` 按照顺序执行过滤器。
*
* @param request 请求对象,表示客户端的 HTTP 请求。会被封装为 `FirewalledRequest` 进行防火墙处理。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。会被封装为 `FirewalledResponse`。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
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);
// 如果没有配置过滤器(filters 为空或 null),则直接继续执行请求链
if (filters == null || filters.size() == 0) {
// 如果启用了 TRACE 日志级别,记录未应用安全的请求信息
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
// 重置请求对象,以便后续过滤器或目标链继续处理
firewallRequest.reset();
// 调用过滤器链,传递处理后的请求和响应
chain.doFilter(firewallRequest, firewallResponse);
return; // 跳出方法,表示不需要进一步的安全处理
}
// 如果启用了 DEBUG 日志级别,记录正在应用安全处理的请求信息
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
// 创建一个虚拟过滤器链,按顺序执行所有配置的过滤器
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
// 使用虚拟过滤器链执行请求处理,确保所有的过滤器都能按顺序执行
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
3)VirtualFilterChain
的 doFilter
方法。
/**
* 过滤器链的核心方法,按顺序执行当前过滤器链中的每个过滤器。
* 该方法负责管理过滤器的执行顺序,并确保请求在处理完当前过滤器后传递到下一个过滤器。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// 判断当前过滤器是否已处理完所有过滤器,如果已处理完所有过滤器,则执行后续操作
if (this.currentPosition == this.size) {
// 如果启用了 DEBUG 日志级别,记录已经安全处理完的请求信息
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
}
// 退出安全过滤器链后,禁用路径剥离操作(防止路径信息丢失)
this.firewalledRequest.reset();
// 调用原始的过滤器链,继续处理请求和响应
this.originalChain.doFilter(request, response);
return; // 结束当前过滤器处理,转交给下一个处理环节
}
// 更新当前过滤器的位置,表示尚未处理所有过滤器
this.currentPosition++;
// 获取下一个需要执行的过滤器
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
// 如果启用了 TRACE 日志级别,记录当前正在调用的过滤器信息
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
this.currentPosition, this.size));
}
// 调用下一个过滤器,递归执行过滤器链
nextFilter.doFilter(request, response, this);
}
4)在 doFilter
方法中,按照过滤器的顺序依次执行。当前背景下,过滤器的执行流程如图:
这里我们重点关注以下几个过滤器:
- UsernamePasswordAuthenticationFilter
- 处理用户名和密码的身份验证请求。通常用于表单登录认证。
- DefaultLoginPageGeneratingFilter
- 生成默认的登录页面,当需要登录时显示给用户。
- ExceptionTranslationFilter
- 捕捉和转换Spring Security中的异常,通常会将这些异常转化为用户友好的响应或页面。
- FilterSecurityInterceptor
- 用于访问控制决策,检查用户是否有权限访问特定的资源或操作。
2、UsernamePasswordAuthenticationFilter
中的执行流程
Filter
的核心代码都在 doFilter
方法中。UsernamePasswordAuthenticationFilter
也是个 Filter
,它的 doFilter
方法在父类 AbstractAuthenticationProcessingFilter
中。
1)AbstractAuthenticationProcessingFilter
的 doFilter
方法调用了重载方法 doFilter
。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
/**
* 过滤器的核心方法,负责处理请求的身份验证。如果需要身份验证,尝试进行身份验证并处理身份验证结果。
* 如果身份验证成功,继续执行过滤器链;如果身份验证失败,则处理失败情况。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断当前请求是否需要身份验证。如果不需要,直接调用过滤器链继续处理请求。
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return; // 结束当前方法,表示请求无需身份验证,已交给后续过滤器处理
}
try {
// 尝试进行身份验证
Authentication authenticationResult = attemptAuthentication(request, response);
// 如果身份验证结果为空,表示身份验证未完成,直接返回,等待子类处理
if (authenticationResult == null) {
return;
}
// 认证成功后,执行身份验证成功时的后续操作(如记录认证信息、设置会话等)
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// 如果身份验证成功且在身份验证成功之前继续过滤器链的执行,则执行链
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);
}
catch (AuthenticationException ex) {
// 如果身份验证失败,处理认证失败的逻辑
unsuccessfulAuthentication(request, response, ex);
}
}
当执行 doFilter 方法时,会先调用 requiresAuthentication
判断当前请求是否需要身份验证。
2)AbstractAuthenticationProcessingFilter
的 requiresAuthentication
方法。
/**
* 检查当前请求是否需要进行身份验证。
* 通过请求匹配器(`requiresAuthenticationRequestMatcher`)判断请求是否需要身份验证。
* 如果请求匹配该匹配器的条件,返回 `true`,表示请求需要认证;否则返回 `false`。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @return 如果请求匹配认证条件,返回 `true`,否则返回 `false`。
*/
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 判断请求是否匹配认证条件(通过请求匹配器进行判断)
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true; // 如果匹配,表示请求需要身份验证
}
// 如果启用了 TRACE 日志级别,记录未匹配认证条件的请求
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
// 如果请求不匹配认证条件,返回 false,表示不需要身份验证
return false;
}
this.requiresAuthenticationRequestMatcher
的值如图:
也就是说AbstractAuthenticationProcessingFilter
的 requiresAuthentication
方法会判断请求是否是 POST /login
,如果请求不匹配的话,代表不需要执行认证流程,requiresAuthentication
方法返回 false
。
AbstractAuthenticationProcessingFilter
的重载方法 doFilter
继续执行 chain.doFilter(request, response);
进入下一个过滤器 DefaultLoginPageGeneratingFilter
。
3、DefaultLoginPageGeneratingFilter
中的执行流程
DefaultLoginPageGeneratingFilter
中实际执行的是重载方法 doFilter(…)
。如果请求是登录 URL 或者登录错误页面或注销成功页面,则生成登录页面的 HTML 内容并返回给客户端。
1)DefaultLoginPageGeneratingFilter
的是重载方法 doFilter(…)
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
/**
* 过滤器方法,用于处理登录错误、注销成功或登录 URL 请求。如果请求是登录相关的请求,
* 则直接返回登录页面的 HTML 内容;否则,将请求传递给下一个过滤器或目标资源。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断当前请求是否为登录错误页面
boolean loginError = isErrorPage(request);
// 判断当前请求是否为注销成功的请求
boolean logoutSuccess = isLogoutSuccess(request);
// 如果请求是登录 URL 或者登录错误页面或注销成功页面,则生成登录页面的 HTML 内容并返回给客户端
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 生成登录页面的 HTML 内容,根据请求类型(登录错误或注销成功)进行相应处理
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
// 设置响应类型为 HTML,并指定字符集为 UTF-8
response.setContentType("text/html;charset=UTF-8");
// 设置响应内容的长度,避免出现响应数据不完整的情况
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
// 将生成的登录页面 HTML 写入响应输出流
response.getWriter().write(loginPageHtml);
// 终止当前方法,避免继续传递请求给后续过滤器链
return;
}
// 如果请求不属于上述类型,继续执行过滤器链,传递请求给下一个过滤器或目标资源
chain.doFilter(request, response);
}
2)isLoginUrlRequest
方法匹配请求是否是 /login
。我们访问的是 /private
,与 /login
不匹配。所以最终 DefaultLoginPageGeneratingFilter
会继续执行过滤器链中的下一个过滤器或目标资源。
private boolean isLoginUrlRequest(HttpServletRequest request) {
return matches(request, this.loginPageUrl);
}
执行完 DefaultLoginPageGeneratingFilter
后会继续执行后边的几个过滤器,最终会执行到 ExceptionTranslationFilter
。
4、ExceptionTranslationFilter
中的执行流程
1)执行重载方法 doFilter(…)
。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
/**
* 过滤器方法,用于捕获过滤器链中的异常并处理 Spring Security 相关的异常。
* 如果异常是 Spring Security 中的认证或授权异常(例如 AuthenticationException 或 AccessDeniedException),
* 会对其进行处理;否则,将异常重新抛出。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 尝试通过过滤器链进行过滤操作,如果一切正常,继续处理请求
chain.doFilter(request, response);
} catch (IOException ex) {
// 如果发生 I/O 异常,直接抛出
throw ex;
} catch (Exception ex) {
// 捕获所有非 I/O 异常
// 尝试从异常链中提取 Spring Security 相关的异常
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
// 尝试从异常链中提取 AuthenticationException(身份验证异常)
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 如果没有找到 AuthenticationException,尝试提取 AccessDeniedException(权限不足异常)
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// 如果既没有 AuthenticationException 也没有 AccessDeniedException,重新抛出原始异常
if (securityException == null) {
rethrow(ex);
}
// 如果响应已经提交,无法处理异常,则抛出新的 ServletException
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
// 处理 Spring Security 的异常(如认证失败或访问权限不足)
handleSpringSecurityException(request, response, chain, securityException);
}
}
2)ExceptionTranslationFilter
的核心逻辑就是继续放行到下一个过滤器,然后捕获下游过滤器可能产生的异常,针对不同的异常类型执行不同的逻辑。具体对应的逻辑我们后边遇到再说。
ExceptionTranslationFilter
执行完后,执行最后一个过滤器 FilterSecurityInterceptor
。
5、FilterSecurityInterceptor
中的执行流程(核心)
1)FilterSecurityInterceptor
的 doFilter
方法中,执行了 invoke(…)
方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
2)FilterSecurityInterceptor
的 invoke
方法中,关键步骤是调用父类的 beforeInvocation
方法。
/**
* 处理安全过滤的核心方法,用于在过滤器链中对请求进行安全检查。
* 该方法执行前置和后置的安全检查,并确保每个请求只被安全过滤器处理一次。
*
* @param filterInvocation 包含当前 HTTP 请求、响应和过滤器链的封装对象。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的错误,抛出该异常。
*/
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 检查当前请求是否已经应用过此过滤器,并且是否需要观察 "每个请求只处理一次" 的机制
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// 该请求已经应用了此过滤器,且 "每个请求只处理一次" 已启用,因此不再进行安全检查
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// 这是请求第一次被调用,执行安全检查
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
// 标记此请求已经应用过此过滤器,避免重复应用
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 进行前置的安全检查,通过调用父类的 beforeInvocation 方法
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
// 将请求交给过滤器链中的下一个过滤器进行处理
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
// 无论请求处理中是否发生异常,都要确保调用 finallyInvocation 进行清理操作
super.finallyInvocation(token);
}
// 进行后置的安全检查,处理安全上下文的清理或状态恢复
super.afterInvocation(token, null);
}
3)FilterSecurityInterceptor
父类是 AbstractSecurityInterceptor
。AbstractSecurityInterceptor
的 beforeInvocation
方法中,关键步骤是调用 attemptAuthorization
方法。
/**
* 在安全对象执行前进行前置处理的方法。该方法负责获取当前的安全上下文、认证信息,
* 检查授权,并支持切换到 "RunAs" 身份。返回一个包含当前状态的 InterceptorStatusToken。
*
* @param object 传入的安全对象,通常是请求或方法调用,包含需要进行安全检查的内容。
* @return InterceptorStatusToken 包含了当前的安全上下文和授权信息。如果不需要进一步处理,可能返回 null。
* @throws IllegalArgumentException 如果传入对象类型与配置的不匹配时抛出异常。
* @throws AuthenticationCredentialsNotFoundException 如果在安全上下文中找不到认证对象时抛出异常。
*/
protected InterceptorStatusToken beforeInvocation(Object object) {
// 检查传入的 object 是否为空
Assert.notNull(object, "Object was null");
// 检查传入对象是否为安全对象的有效类型
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 获取传入对象的安全元数据属性(如访问权限等)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 如果安全属性为空,检查是否允许公共调用
if (CollectionUtils.isEmpty(attributes)) {
Assert.isTrue(!this.rejectPublicInvocations,
() -> "Secure object invocation " + object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
// 如果允许公共访问,则记录调试日志并发布公共访问事件
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized public object %s", object));
}
publishEvent(new PublicInvocationEvent(object));
return null; // 不需要进一步处理,返回 null
}
// 如果当前没有认证信息,抛出认证未找到的异常
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(this.messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"), object, attributes);
}
// 如果需要,进行用户身份认证
Authentication authenticated = authenticateIfRequired();
// 调试日志:记录正在进行的授权操作
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
}
// 尝试对传入对象进行授权
attemptAuthorization(object, attributes, authenticated);
// 授权成功后,记录调试日志
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
}
// 如果配置了发布授权成功事件,发布授权成功事件
if (this.publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// 尝试以不同的身份 ("RunAs") 运行当前用户
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
// 获取当前安全上下文
SecurityContext origCtx = SecurityContextHolder.getContext();
// 创建一个空的安全上下文,并将身份切换到新的 RunAs 认证
SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
newCtx.setAuthentication(runAs);
SecurityContextHolder.setContext(newCtx);
// 记录切换后的身份信息
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
}
// 返回包含原始安全上下文的 InterceptorStatusToken,以便在方法调用后恢复身份
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
// 如果 RunAsManager 没有返回身份切换,则记录调试日志
this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
// 返回当前上下文的 InterceptorStatusToken,标记不需要身份切换
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
4)AbstractSecurityInterceptor
的 attemptAuthorization
方法,关键步骤是调用 this.accessDecisionManager.decide(...)
。调用 AccessDecisionManager
来决定用户是否有权限访问该资源。
/**
* 尝试对当前用户进行授权检查。
* 该方法会将授权决策委托给 AccessDecisionManager,后者根据用户的认证信息和资源所需的安全属性来判断
* 用户是否有权限访问指定的资源。
*
* @param object 需要访问的资源对象,通常是 HTTP 请求等。
* @param attributes 与该资源相关的安全配置属性(ConfigAttribute),描述了访问该资源所需的权限。
* @param authenticated 当前用户的认证对象,包含了用户的凭证和权限信息。
*/
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
// 调用 AccessDecisionManager 来决定用户是否有权限访问该资源
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException ex) {
// 如果日志级别为 trace,记录详细的授权失败信息,包括所使用的 AccessDecisionManager
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("授权失败,资源: %s,所需权限: %s,使用的决策管理器: %s", object,
attributes, this.accessDecisionManager));
}
// 如果日志级别为 debug,仅记录授权失败信息和资源
else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("授权失败,资源: %s,所需权限: %s", object, attributes));
}
// 触发授权失败事件,传递资源、所需权限、用户认证信息及异常
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
// 抛出异常,表示授权失败
throw ex;
}
}
5)这里的AccessDecisionManager
是其实现类 AffirmativeBased
的 decide
方法。
/**
* 根据多个 AccessDecisionVoter 的投票结果,决定是否授予访问权限。
* 此方法遍历所有投票者,并根据每个投票者的结果来做出最终的访问决策。
*
* @param authentication 当前用户的认证信息,包括凭证和权限。
* @param object 需要保护的资源或对象(通常是请求对象)。
* @param configAttributes 资源所需的权限集合(ConfigAttribute)。
* @throws AccessDeniedException 如果用户被拒绝访问资源,则抛出此异常。
*/
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException {
int deny = 0; // 记录被拒绝投票的次数
// 遍历所有的 AccessDecisionVoter 进行投票
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 投票结果:ACCESS_GRANTED、ACCESS_DENIED 或 ACCESS_ABSTAIN
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
// 如果有投票者授予访问权限,立即返回,表示访问允许
return;
case AccessDecisionVoter.ACCESS_DENIED:
// 记录被拒绝的投票
deny++;
break;
default:
// 其他情况,继续循环(ACCESS_ABSTAIN)
break;
}
}
// 如果有任何投票者拒绝访问,则抛出 AccessDeniedException
if (deny > 0) {
throw new AccessDeniedException(
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// 如果所有投票者都选择了弃权,检查是否允许访问
checkAllowIfAllAbstainDecisions();
}
最终返回的是 ACCESS_DENIED
。
所以会在
AccessDecisionManager
的实现类 AffirmativeBased
的 decide
方法中抛出 AccessDeniedException
。
6)在AccessDecisionManager
的实现类 AffirmativeBased
的 decide
方法中抛出 AccessDeniedException
后,层层抛出,直到在 ExceptionTranslationFilter
中被捕获,执行对应的处理逻辑。
6、ExceptionTranslationFilter
中的异常处理逻辑
1)我们重新回到 ExceptionTranslationFilter
的 doFilter
方法。当 AccessDeniedException
被捕获后,最终会执行到 handleSpringSecurityException
方法。
/**
* 过滤器方法,用于捕获过滤器链中的异常并处理 Spring Security 相关的异常。
* 如果异常是 Spring Security 中的认证或授权异常(例如 AuthenticationException 或 AccessDeniedException),
* 会对其进行处理;否则,将异常重新抛出。
*
* @param request 请求对象,表示客户端的 HTTP 请求。
* @param response 响应对象,表示服务器向客户端返回的 HTTP 响应。
* @param chain 过滤器链,允许调用下一个过滤器或最终的目标资源。
* @throws IOException 如果在处理请求时发生 I/O 错误,抛出该异常。
* @throws ServletException 如果在处理请求时发生 Servlet 相关的异常,抛出该异常。
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 尝试通过过滤器链进行过滤操作,如果一切正常,继续处理请求
chain.doFilter(request, response);
} catch (IOException ex) {
// 如果发生 I/O 异常,直接抛出
throw ex;
} catch (Exception ex) {
// 捕获所有非 I/O 异常
// 尝试从异常链中提取 Spring Security 相关的异常
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
// 尝试从异常链中提取 AuthenticationException(身份验证异常)
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 如果没有找到 AuthenticationException,尝试提取 AccessDeniedException(权限不足异常)
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// 如果既没有 AuthenticationException 也没有 AccessDeniedException,重新抛出原始异常
if (securityException == null) {
rethrow(ex);
}
// 如果响应已经提交,无法处理异常,则抛出新的 ServletException
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
// 处理 Spring Security 的异常(如认证失败或访问权限不足)
handleSpringSecurityException(request, response, chain, securityException);
}
}
2)ExceptionTranslationFilter
的 handleSpringSecurityException
方法。异常类型是 AccessDeniedException
,执行 handleAccessDeniedException(...)
方法。
/**
* 处理访问拒绝异常(AccessDeniedException),根据用户的认证状态决定是重定向到认证入口点,还是调用访问拒绝处理器。
* 该方法检查当前用户是否为匿名用户或记住我用户,如果是,则重定向到认证入口点;否则,调用访问拒绝处理器处理该请求。
*
* @param request 当前 HTTP 请求对象。
* @param response 当前 HTTP 响应对象。
* @param chain 当前请求的 FilterChain。
* @param exception 发生的 AccessDeniedException 异常。
* @throws ServletException 如果处理过程中发生 Servlet 异常。
* @throws IOException 如果处理过程中发生输入输出异常。
*/
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// 获取当前认证对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断当前用户是否为匿名用户或者记住我用户
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
// 如果是匿名用户或记住我用户,重定向到认证入口点
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
// 记录日志
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
// 发送开始认证请求,提示需要完全认证才能访问资源
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
// 如果不是匿名用户且已经认证,调用访问拒绝处理器
else {
// 记录日志
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
// 交给自定义的 accessDeniedHandler 处理访问拒绝
this.accessDeniedHandler.handle(request, response, exception);
}
}
3)当前用户为匿名用户,进入 ExceptionTranslationFilter
的 sendStartAuthentication
方法。核心代码是调用了 AuthenticationEntryPoint
的 commence
方法。具体执行的是 AuthenticationEntryPoint
接口的实现类 DelegatingAuthenticationEntryPoint
的 commence
方法。
/**
* 处理开始认证的逻辑。当用户访问需要完全认证的资源时,清除当前的认证信息并重定向到认证入口点。
* 该方法会清空当前的 `SecurityContext`,保存当前请求,然后通过认证入口点开始认证过程。
*
* @param request 当前 HTTP 请求对象。
* @param response 当前 HTTP 响应对象。
* @param chain 当前请求的 FilterChain。
* @param reason 认证失败的原因,通常是一个 `AuthenticationException`。
* @throws ServletException 如果处理过程中发生 Servlet 异常。
* @throws IOException 如果处理过程中发生输入输出异常。
*/
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: 清空当前的 SecurityContext 中的认证信息,因为现有的认证信息已经不再有效
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context); // 设置一个新的空的 SecurityContext
// 保存当前的请求,以便认证完成后可以重定向回原始请求
this.requestCache.saveRequest(request, response);
// 调用认证入口点,开始认证流程,通常会重定向到登录页面
this.authenticationEntryPoint.commence(request, response, reason);
}
4)DelegatingAuthenticationEntryPoint
的 commence
方法。
/**
* 实现 `AuthenticationEntryPoint` 接口的 `commence` 方法,处理认证失败时的逻辑。
* 它遍历一组预定义的 `RequestMatcher` 来匹配当前请求,找到匹配的入口点后执行相应的认证处理。
* 如果没有找到匹配的入口点,默认使用配置的默认认证入口点来处理请求。
*
* @param request 当前 HTTP 请求对象。
* @param response 当前 HTTP 响应对象。
* @param authException 认证异常,表示认证失败的原因。
* @throws IOException 如果处理过程中发生输入输出异常。
* @throws ServletException 如果处理过程中发生 Servlet 异常。
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 遍历所有已配置的 RequestMatcher,尝试找到匹配当前请求的入口点
for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
// 如果找到匹配的 RequestMatcher,则执行相应的 AuthenticationEntryPoint
if (requestMatcher.matches(request)) {
AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
logger.debug(LogMessage.format("Match found! Executing %s", entryPoint));
entryPoint.commence(request, response, authException);
return; // 一旦找到匹配的 EntryPoint,就执行完相应逻辑并返回
}
}
// 如果没有找到匹配的 EntryPoint,则使用默认的 EntryPoint 来处理认证失败
logger.debug(LogMessage.format("No match found. Using default entry point %s", this.defaultEntryPoint));
this.defaultEntryPoint.commence(request, response, authException);
}
5)通过 LoginUrlAuthenticationEntryPoint
告诉客户端重定向到 /login
转载自:https://juejin.cn/post/7425803259443707943