ToB企服应用市场:ToB评测及商务社交产业平台
标题:
Spring Security之安全异常处理
[打印本页]
作者:
南七星之家
时间:
2024-8-9 15:46
标题:
Spring Security之安全异常处理
前言
在我们的安全框架中,不管是什么框架(包括通过过滤器自定义)都须要处理涉及安全相关的异常,比方:登录失败要跳转到登录页,访问权限不足要返回页面亦或是json。接下来,我们就看看Spring Security是怎么处理异常的!
什么是异常处理
在Spring Security中,特指对于安全异常的处理。
我们知道Spring Security主要是基于过滤器来实现的,因此每个安全过滤器都大概发生安全异常,以是处理逻辑会被散落在各个过滤器中。
Spring自然是不能忍受这种设计,于是就有了专门的安全异常处理。
注:下文我们都用异常处理来代指安全异常处理。
异常处理设计
Spring Security将安全异常分为两类。
AuthenticationException —— 认证异常
认证异常触发原因描述BadCredentialsException无法识别凭证大概是没有凭证/无法解密/格式不对等UsernameNotFoundException没有找到用户用户名没有对应的账号SessionAuthenticationException认证过程中与session相关的校验。比方,控制多点登录时,当某用户多点登录超过规定命量就会发生session认证异常AuthenticationServiceException认证服务碰到无法处理的情况是触发认证服务异常ProviderNotFoundExceptionProviderManager没有配置任何的Provider没有ProviderPreAuthenticatedCredentialsNotFoundException与第三方认证系统集成时,发现客户端没有传凭证前认证凭证没有找到AuthenticationCredentialsNotFoundException这个主要是
鉴权
的时候发现没有认证,就会抛出没有找到认证凭证。RememberMeAuthenticationException-主要与记住我功能,恢复登录态有关NonceExpiredException-这个主要与Digest认证方式有关AccountStatusException校验账号状态时触发账号状态异常
AccessDeniedException —— 访问拒绝异常
访问拒绝异常触发原因描述AuthorizationServiceException碰到无法处理的鉴权时触发比方配置错误,数据范例错误CrsfException防御Crsf时触发这里有两个,分别对应WebFlux和WebMvc实在对于鉴权来说,只要发现权限不满意,都是直接抛出AccessDeniedException的。
认证异常和访问拒绝异常的区别
与访问拒绝异常相比,认证异常要复杂不少。这是由认证过程和认证方式的多样性导致的。
认证过程:
一个完整的用户暗码认证过程各组件的调用关系和简化
组件都有不少,更何况要捋清晰调用关系。上面也只能是给各人看看认证过程中须要干啥,有哪些组件负责。
认证方式:
这个就不多啰嗦,前面说认证过滤器的时候有说过。
异常处理器
异常类定义异常处理器认证异常AuthenticationExceptionAuthenticationFailureHandler访问异常AccessDeniedExceptionAccessDeniedHandler 为什么要搞两个异常,还要搞两个组件来处理呢?
从安全业务上说,本来就是两种业务,访问跟认证是两个事情。
从单一职责原则来说,肯定要进行拆分,因为这两个组件处理的是不同的异常。
一般而言,登录异常我们是须要重定向到登录页面的,而接口访问异常则不然,一般通过返回错误拒绝哀求。
实际上,这两个组件定义的可以说一模一样:
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException;
}
public interface AccessDeniedHandler {
void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException;
}
复制代码
除了方法名,和入参的异常不同,其他的都是一样的。乃至,如果我们进一步看看异常的定义的话,连异常定义也是类似的,都是继承于RuntimeException,没有任何其他多余的字段和逻辑。
AuthenticationFailureHandler的实现
AuthenticationFailureHandler描述AuthenticationEntryPointFailureHandler通过AuthenticationEntryPoint组件处理SimpleUrlAuthenticationFailureHandler重定向到指定URL,如果没有指定,则退化返回401ForwardAuthenticationFailureHandler重定向到指定的URL,必须指定URLExceptionMappingAuthenticationFailureHandler通过匹配异常寻找对应的处理器,一般由用户自行配置。DelegatingAuthenticationFailureHandler委托其他的处理器处理 这里有一个特殊的,他利用另一个组件
AuthenticationEntryPoint
进行处理。
AuthenticationEntryPoint
Http403ForbiddenEntryPoint
他是处理登录异常的通用的可选方案,通常是AbstractPreAuthenticatedProcessingFilter(基于外部认证服务器进行认证)。焦点逻辑:总是返回403。
这个实现是用来兜底的,如果找不到其他的,那就会用他。
HttpStatusEntryPoint
他是一种可选方案,直接返回一个用户指定的http状态,response.setStatus(this.httpStatus.value())。
LoginUrlAuthenticationEntryPoint
如果我们利用的是UsernamePasswordAuthenticationFilter,那么默认利用的就是这个。其焦点逻辑也比较简单明了,就是重定向到登录页面。如果我们往上一层对比到SimpleUrlAuthenticationFailureHandler 、ForwardAuthenticationFailureHandler ,他的区别在于如果我们指定了loginPage,那么就会利用他。他会自动识别是绝对地址还是相对地址进行拼接。
DigestAuthenticationEntryPoint
显然,他是为DigestAuthenticationFilter服务的。他会设置一些与Digest相关的哀求头,然后调用response.sendError方法处理。
BasicAuthenticationEntryPoint
为BasicAuthenticationFilter服务。焦点逻辑与Digest类似,也是设置相关哀求头,通过response.sendError方法处理。
DelegatingAuthenticationEntryPoint
委托。没有自己的逻辑,而是交给别的AuthenticationEntryPoint。
AccessDeniedHandler的实现
AccessDeniedHandlerImpl
基础实现,也是默认实现。设置HTTP错误码-403,并转发到错误页面。
InvalidSessionAccessDeniedHandler
显然是为了处理session失效异常的。不过风趣的是,官方在CsrfConfigurer中引入这个。并且是为了处理MissingCsrfTokenException的。并且为了单一职责,还构建了下面的委托处理器。
DelegatingAccessDeniedHandler
委托处理器。他管理着哪些异常对应哪个处理器,并将当前异常的处理交付给对应的处理器处理。故而得名“委托”处理器,算是个代理人吧。当然,他要求必须有个兜底的默认处理器。
RequestMatcherDelegatingAccessDeniedHandler
他也是委托处理器,不同点在于,他是RequestMatcherDelegating,也即基于RequestMatcher进行Request匹配处理器。
ObservationMarkingAccessDeniedHandler
他是用来统计数据的,观察标志。
CompositeAccessDeniedHandler
组合模式的实现,用来管理多个处理器。目前看的话,主要是为了统计服务,因为他会调用每一个处理器,这大概会出现问题。只有统计这个处理器,须要其他的处理器来实现真正的处理,须要配合。
到这里问一句,这里我们看到了几种设计模式?策略模式、委托模式、组合模式。可以看到Spring对于代码的追求,这也是我们阅读源码的目标之一,学习好的设计。而这背后都是设计原则。
异常处理原理
前面我们大概相识了异常处理的来龙去脉,知道了其焦点组件。现在我们来深入相识其原理。
认证异常处理原理
要明白这个,就必须回顾一下认证流程(这里以默认的用户暗码登录为例):
AbstractAuthenticationProcessingFilter#doFilter
> UsernamePasswordAuthenticationFilter#attemptAuthentication
|-> ProviderManager#authenticate
|-|-> AbstractUserDetailsAuthenticationProvider#authenticate
|-|-|->DaoAuthenticationProvider#retrieveUser
|-|-|-|->JdbcDaoImpl#loadUserByUsername
|-|-|->DaoAuthenticationProvider#additionalAuthenticationChecks
|-|-|->DaoAuthenticationProvider#createSuccessAuthentication
|-|-|->AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication
// 同层级的表示顺序调用,不同层级的:上层方法调用下层方法,是递进关系。层级减少表示方法返回
复制代码
负责处理认证哀求的AbstractAuthenticationProcessingFilter#doFilter方法中会捕捉异常,并交给unsuccessfulAuthentication方法处理。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// 1. 清空安全上下文
this.securityContextHolderStrategy.clearContext();
// 2. 记住我功能,清空cookie,认证失败的处理
this.rememberMeServices.loginFail(request, response);
// 3. 通过认证失败处理处理
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
复制代码
默认情况下,会利用SimpleUrlAuthenticationFailureHandler重定向到登录页面。
什么?怎么知道是这个处理器?行吧,我们来看看FormLoginConfigurer的源码吧。
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
public FormLoginConfigurer() {
// 调用父类构造器
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
/**
* 在配置过目标过滤器之前,会先调用这个方法进行Configurer的初始化
*/
@Override
public void init(H http) throws Exception {
// 初始化父类
super.init(http);
// 初始化默认的登录页面
initDefaultLoginFilter(http);
}
}
public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
extends AbstractHttpConfigurer<T, B> {
protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
// 调用另一个构造器
this();
this.authFilter = authenticationFilter;
if (defaultLoginProcessingUrl != null) {
// 由于FormLoginConfigurer的构造器中传的是null,因此不会走到这
// 当然,由于这个方法是public,因此也可以在配置时被我们调用
// 他无非就是指定什么地址是认证请求罢了
loginProcessingUrl(defaultLoginProcessingUrl);
}
}
protected AbstractAuthenticationFilterConfigurer() {
// 构造器中设置登录页面uri
setLoginPage("/login");
}
private void setLoginPage(String loginPage) {
this.loginPage = loginPage;
// 指定AuthenticationEntryPoint,后面异常处理过滤器用的是这个来处理没有登录的异常。
this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}
@Override
public void init(B http) throws Exception {
// 更新/初始化认证相关的默认组件
updateAuthenticationDefaults();
// 更新访问权限-认证页面、认证请求、认证失败
updateAccessDefaults(http);
// 注册默认的AuthenticationEntryPoint
registerDefaultAuthenticationEntryPoint(http);
}
protected final void updateAuthenticationDefaults() {
if (this.loginProcessingUrl == null) {
loginProcessingUrl(this.loginPage);
}
if (this.failureHandler == null) {
// 指定默认的异常跳转页面
failureUrl(this.loginPage + "?error");
}
LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
}
}
public final T failureUrl(String authenticationFailureUrl) {
// 就是这个啦
T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
// 所谓注册就是注册到异常处理过滤器上
// 思考个问题:下面这种处理方式不就耦合ExceptionHandlingConfigurer了吗?
// 为什么不是像其他的sharedObject那样,直接放到HttpSecurityd#sharedObjects中,在ExceptionHandlingConfigurer再自行获取设置。
// 答:如果这样的话,会导致BUG。init方法是在执行了用户配置方法之后在HttpSecurity构建过滤器链的时候调用的。有可能将用户配置的覆盖了。
ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
getAuthenticationEntryPointMatcher(http));
}
}
复制代码
从FormLoginConfigurer出发,我们知道UsernamePasswordAuthenticationFilter利用的是SimpleUrlAuthenticationFailureHandler,同时ExceptionTranslationFilter利用的是LoginUrlAuthenticationEntryPoint。但这个设计我没有很明白,个人觉得应该在顶层都利用AuthenticationFailureHandler才合理。不知道是不是为了区分场景。
场景一:登录处理时发生的异常,直接被捕捉处理了。
场景二:是鉴权时发现没有任何凭证,由异常处理过滤器处理。
但SimpleUrlAuthenticationFailureHandler、LoginUrlAuthenticationEntryPoint,内部处理没有太大区别,都是为了跳转到登录页面。
访问拒绝异常处理原理
鉴权相关的,之前我们聊过,忘记的同学可以通过下面的链接再回想复习一下。大概文章的标题大概说的权限配置,但同时也从原理上给各人分析了如何鉴权的。也正是因为有两种方式,以是没有单独写鉴权过滤器。因为基于HttpRequest的配置方式的鉴权原理是通过AuthorizationFilter,也就是过滤器实现的。而另一种权限配置方式-基于方法配置权限-则是通过AOP实现的。
Spring Security之基于方法配置权限
Spring Security之基于HttpRequest配置权限
ExceptionTranslationFilter
他主要负责处理的是鉴权过程中发生的异常。这里就包括用户权限不足的AccessDeniedException,以及鉴权时发现用户还没有登录而抛出的认证异常。
public class AuthorizationFilter extends GenericFilterBean {
private Authentication getAuthentication() {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
}
}
复制代码
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// 尝试从异常堆栈中找到安全异常
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
// 不是安全异常,直接重新抛出
rethrow(ex);
}
if (response.isCommitted()) {
// 如果response已经提交,则抛出servlet异常
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
// 处理安全异常
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// 处理认证异常
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
// 处理访问异常
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
// 这里是转换方法名,是一种代码追求、也是一种代码的自解释:对于上层方法的作用是处理认证异常,而处理认证异常的手段是发送开始认证(其实就是跳转到登录页面开始登录流程),因为走到这的都是鉴权时不存在凭证导致的认证异常。
sendStartAuthentication(request, response, chain, exception);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
// 对于匿名用户或者不是记住我用户,直接跳转登录页开始登录流程
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
// 正常用户则通过AccessDeniedHandler处理
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
// 清空安全上下文
this.securityContextHolderStrategy.setContext(context);
// 记录当前权限不足的请求,登录成功后可能需要自动跳转
this.requestCache.saveRequest(request, response);
// 通过AuthenticationEntryPoint处理,这里是跳转到登录页面
this.authenticationEntryPoint.commence(request, response, reason);
}
}
复制代码
总结
异常处理体系包括
异常定义分两类 —— 认证异常、访问拒绝异常(鉴权异常)
异常处理器 —— AuthenticationFailureHandler、AccessDeniedHandler分别对应异常分类
异常的处理 —— 认证过滤器和异常处理器
认证异常的处理一般是跳转到登录页面。
访问异常的处理默认则是AccessDeniedHandlerImpl处理,发送403错误码或者跳转到错误页。
后记
前阵子搬家,一直在适应新屋子的生活节奏,拖了不少时间,对不住了各位。后面应该会回复正常。
至此,咱们聊了认证、鉴权、session、异常处理,接下来咱们聊聊认证过程中一些小的功能点,比方:登录后跳转到之前异常的哀求、RemenberMe、多处登录控制。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4