Bootstrap

SpringSecurity源码学习二:异常处理

1. 原理

Spring Security 异常处理的原理是通过一系列的异常处理器来处理在安全验证和授权过程中可能出现的异常。当发生异常时,Spring Security会根据异常的类型和配置的处理器来确定如何处理异常。

异常处理的原理可以概括为以下几个步骤:

  1. 认证过程中的异常处理:在用户进行身份验证时,可能会发生各种异常,例如用户名或密码错误、账户锁定等。Spring Security使用AuthenticationManager来处理认证过程中的异常,根据异常类型和配置的AuthenticationProvider来确定如何处理异常。

  2. 授权过程中的异常处理:在用户通过认证后,访问受保护资源时可能会发生授权异常,例如用户没有足够的权限访问资源。Spring Security使用AccessDecisionManager来处理授权过程中的异常,根据异常类型和配置的AccessDecisionVoter来确定如何处理异常。

  3. 异常转换和处理:Spring Security还提供了ExceptionTranslationFilter来处理身份验证和授权过程中的异常。该过滤器会捕获异常,并根据异常类型和配置的处理器,例如AuthenticationEntryPoint和AccessDeniedHandler,来进行适当的转换和处理。AuthenticationEntryPoint用于处理未经身份验证的请求,AccessDeniedHandler用于处理已经身份验证但无权访问资源的请求。

2. 组件

在Spring Security中,异常处理通常涉及以下几个关键组件:

  1. AuthenticationEntryPoint(认证入口点):用于处理未经身份验证的请求。当用户尝试访问受保护的资源时,如果尚未进行身份验证,AuthenticationEntryPoint将负责返回相应的响应,例如要求用户进行身份验证或返回错误消息。

  2. AccessDeniedHandler(访问拒绝处理器):用于处理已经身份验证但无权访问受保护资源的请求。如果用户已经通过身份验证,但没有足够的权限访问某个资源,AccessDeniedHandler将负责返回相应的响应,例如返回自定义的错误页面或错误消息。

  3. ExceptionTranslationFilter(异常转换过滤器):作为过滤器链中的一个关键组件,ExceptionTranslationFilter负责捕获Spring Security中的异常,并将其转换为适当的响应。它使用AuthenticationEntryPoint和AccessDeniedHandler来处理不同类型的异常,并提供合适的响应。

通过配置这些组件,您可以自定义异常处理的行为,以满足您的应用程序需求。您可以指定自定义的AuthenticationEntryPoint和AccessDeniedHandler,并将它们与Spring Security的过滤器链进行集成,以实现自定义的异常处理逻辑。

3. ExceptionTranslationFilter

当身份验证失败或访问被拒绝时,ExceptionTranslationFilter会拦截相应的异常,并根据异常的类型和配置的处理器来进行适当的处理。它根据异常类型分别使用AuthenticationEntryPoint和AccessDeniedHandler来处理不同的异常情况。

3.1 默认过滤器顺序

过滤器链在初始化的时候会默认加载好一些默认的过滤器,其中就包括ExceptionTranslationFilter过滤器。ExceptionTranslationFilter在过滤器链倒数第二个,最后一个是FilterSecurityInterceptor。FilterSecurityInterceptor是用来获取所有配置资源的访问授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

3.2 ExceptionTranslationFilter源码

	//问拒绝处理 就是你要访问某个资源,但是当你没有访问权限时,就会抛出异常,在此类中进行处理。
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

	//顾名思义身份验证入口,主要用来判断你的身份,凡是在身份认证过程中发生的错误
	private AuthenticationEntryPoint authenticationEntryPoint;

	//用以判断SecurityContextHolder中所存储信息 判断上下文中有无用户信息来抛出异常
	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

这是此类中的三个比较重要的私有字段,其中accessDeniedHandler用来处理权限异常;authenticationEntryPoint用来处理认证异常;authenticationTrustResolver用以判断SecurityContextHolder中所存储信息 判断上下文中有无用户信息来抛出异常。其中accessDeniedHandler和authenticationEntryPoint异常处理在ExceptionTranslationFilter过滤器中处理。

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			//执行下一个过滤器逻辑,也就是SecurityContextHolder过滤器。
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			//获取全部异常
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			//判断异常的类型 是否为AuthenticationException
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (securityException == null) {
				//判断是否为AccessDeniedException
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			//如果不是上边两个异常,继续抛出,说明此过滤器只处理AuthenticationException和AccessDeniedException异常
			if (securityException == null) {
				rethrow(ex);
			}
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception "
						+ "because the response is already committed.", ex);
			}
			//处理异常
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

chain.doFilter(request, response)会执行执行下一个过滤器逻辑,也就是SecurityContextHolder过滤器。当用户未认证或者接口未授权时,会被catch到执行异常处理。处理报错的核心逻辑在handleSpringSecurityException()方法中。

	private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, RuntimeException exception) throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			//处理AuthenticationException异常
			handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			//处理AccessDeniedException异常
			handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
		}
	}

异常分为两部分:handleAuthenticationException处理AuthenticationException异常,handleAccessDeniedException处理AccessDeniedException异常。

3.2.1 AuthenticationException异常

	private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
			FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
		this.logger.trace("Sending to authentication entry point since authentication failed", exception);
		sendStartAuthentication(request, response, chain, 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
		SecurityContextHolder.getContext().setAuthentication(null);
		this.requestCache.saveRequest(request, response);
		//调用authenticationEntryPoint子类的commence方法  我们自定义authenticationEntryPoint的子类
		this.authenticationEntryPoint.commence(request, response, reason);
	}

可以看到,最终会调用AuthenticationEntryPoint的commence方法。AuthenticationEntryPoint是一个接口,有默认的实现类,同时我们也可以自定义authenticationEntryPoint。

自定义authenticationEntryPoint

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 用户自定义返回
    }
}

3.2.2 AccessDeniedException异常

	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);
		//如果是游客登录或者是rememberMe用户,则重新登录
		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);
			}
			//执行我们的权限认证错误自定义逻辑
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}

此代码最终调用的是AccessDeniedHandler的handle方法,同样的AccessDeniedHandler是接口,同时有默认实现类,也可以自定义实现。

自定义AccessDeniedHandler

public class MyAccessDeniedHandler implements AccessDeniedHandler {

   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
      //用户自定义实现
   }
}

总结

  1. 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其他过滤器。

  2. 如果用户没有被认证,或者是一个 AuthenticationException,那么就开始认证,SecurityContextHolder 被清理掉。HttpServletRequest 被保存起来,这样一旦认证成功,它就可以用来重放原始请求。
    AuthenticationEntryPoint 用于请求客户的凭证。例如,它可以重定向到一个登录页面或发送一个 WWW-Authenticate 头。

  3. 如果是 AccessDeniedException,那么就是 Access Denied。AccessDeniedHandler 被调用来处理拒绝访问(access denied)。

;