本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。

        异常也算是开发中一个不可避免的问题,Spring Security中关于异常的处理主是两方面:认证异常处理、权限异常处理。除此之外的异常抛出,交给Spring去处理。这篇文章主要学习的知识点:Spring Security异常体系、ExceptionTranslationFilter、自定义异常配置。

Spring Security异常体系

        Spring Security中的异常主要分为两大类:

  • AuthenticationException
  • AccessDeniedException

        其中认证异常涉及 的异常类比较多,下表展示了Spring Security中的所有认证的异常

Spring Security认证异常类

异常类型

备注

AuthenticationException

认证异常的父类,抽象类

BadCredentialsException

登录凭证(密码)异常

InsufficientAuthenticationException

登录凭证不够充分而抛出的异常

SessionAuthenticationException

会话并发管理时抛出的异常,例如会话总数超出最大限制数

UsernameNotFoundException

用户名不存在异常

PreAuthenticatedCredentialsNotFoundException

身份预认证失败异常

ProviderNotFoundException

未配置AuthenticationProvider 异常

AuthenticationServiceException

由于系统问题而无法处理认证请求异常。

InternalAuthenticationServiceException

由于系统问题而无法处理认证请求异常。和AuthenticationServiceException 不同之处在于,如果外部系统出错,则不会抛出该异常

AuthenticationCredentialsNotFoundException

SecurityContext中不存在认证主体时抛出的异常

NonceExpiredException

HTTP摘要认证时随机数过期异常

RememberMeAuthenticationException

RememberMe认证异常

CookieTheftException

RememberMe认证时Cookie 被盗窃异常

InvalidCookieException

RememberMe认证时无效的Cookie异常

AccountStatusException

账户状态异常

LockedException

账户被锁定异常

DisabledException

账户被禁用异常

CredentialsExpiredException

登录凭证(密码)过期异常

AccountExpiredException

账户过期异常

        相比于认证异常,权限异常类就少了很多,下表展示了Spring Security中的权限异常类:

Spring Security权限异常类

异常类型

备注

AccessDeniedException

权限异常的父类

AuthorizationServiceException

由于系统问题而无法处理权限时抛出异常

CsrfException

Csrf令牌异常

MissingCsrfTokenException

Csrf令牌缺失异常

InvalidCsrfTokenException

Csrf令牌无效异常

        实际项目中,如果Spring Security提供的这些异常类无法满足需求,开发者可以根据实际需求自定义异常类。

ExceptionTranslationFilter原理分析

        Spring Security中的异常处理主要是在ExceptionTranslationFilter过滤器中完成的,该过滤器主要处理AuthenticationException和AccessDeniedException类型的异常。其他异常则会继续抛出,交给上一层容器去处理。

        接下来我们分下ExceptionTranslationFilter的工作原理。在WebSecurityConfigurerAdapter的getHttp方法中进行初始化HttpSecurity的时候,调用了applyDefaultConfiguration方法,然后applyDefaultConfiguration方法中调用了HttpSecurity的exceptionHandling方法。

protected final HttpSecurity getHttp() throws Exception {
		if (this.http != null) {
			return this.http;
		}

		//省略 .....

		if (!this.disableDefaults) {
			applyDefaultConfiguration(this.http);
			ClassLoader classLoader = this.context.getClassLoader();
			List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
					.loadFactories(AbstractHttpConfigurer.class, classLoader);
			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				this.http.apply(configurer);
			}
		}
		configure(this.http);
		return this.http;
	}


	private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
		http.csrf();
		http.addFilter(new WebAsyncManagerIntegrationFilter());
		http.exceptionHandling();
		http.headers();
		http.sessionManagement();
		http.securityContext();
		http.requestCache();
		http.anonymous();
		http.servletApi();
		http.apply(new DefaultLoginPageConfigurer<>());
		http.logout();
	}

exceptionHandling方法就是调用ExceptionHandlingConfigurer去配置ExceptionTranslationFilter,对于ExceptionHandlingConfigurer类而言,最重要的就是其configure方法:

@Override
	public void configure(H http) {
		AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
		ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
				getRequestCache(http));
		AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
		exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
		exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
		http.addFilter(exceptionTranslationFilter);
	}

        可以看到,这里首先获得一个AuthenticationEntryPoint 的实例对象entryPoint,这就是认证失败时的处理器,然后创建一个ExceptionTranslationFilter的实例对象exceptionTranslationFilter并传入entryPoint。接下来创建一个AccessDeniedHandler的实例对象deniedHandler设置个体exceptionTranslationFilter。最后调用postProcess方法,将ExceptionTranslationFilter过滤器注册到Spring容器中,然后调用addFilter方法将其添加到Spring Security过滤器链中。

AuthenticationEntryPoint

        AuthenticationEntryPoint的实例对象时通过getAuthenticationEntryPoint方法创建的,我们看下该方法的逻辑:

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
		AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
		if (entryPoint == null) {
			entryPoint = createDefaultEntryPoint(http);
		}
		return entryPoint;
	}


	private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
		if (this.defaultEntryPointMappings.isEmpty()) {
			return new Http403ForbiddenEntryPoint();
		}
		if (this.defaultEntryPointMappings.size() == 1) {
			return this.defaultEntryPointMappings.values().iterator().next();
		}
		DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
				this.defaultEntryPointMappings);
		entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
		return entryPoint;
	}

RequestMatcher,如果满足子使用对应的认证失败处理器来处理。我们看看DelegatingAuthenticationEntryPoint的commemce方法:

@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
        //挨个遍历当前的请求是否满足当前的请求,如果满足就调用AuthenticationEntryPoint的commence方法
		for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
			logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
			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;
			}
		}
		logger.debug(LogMessage.format("No match found. Using default entry point %s", this.defaultEntryPoint));
		//没有匹配带就调用默认的认证异常处理器
		this.defaultEntryPoint.commence(request, response, authException);
	}

AccessDeniedHandler

        我们再来看看AccessDeniedHandler实例的获取流程其实和AuthenticationEntryPoint的获取流程基本上是一致的。

AccessDeniedHandler getAccessDeniedHandler(H http) {
		AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
		if (deniedHandler == null) {
			deniedHandler = createDefaultDeniedHandler(http);
		}
		return deniedHandler;
	}


	private AccessDeniedHandler createDefaultDeniedHandler(H http) {
		if (this.defaultDeniedHandlerMappings.isEmpty()) {
			return new AccessDeniedHandlerImpl();
		}
		if (this.defaultDeniedHandlerMappings.size() == 1) {
			return this.defaultDeniedHandlerMappings.values().iterator().next();
		}
		return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
				new AccessDeniedHandlerImpl());
	}

        不同的是,defaultDeniedHandlerMappings变量是空的,所以最终获取到的实例是AccessDeniedHandlerImpl。在AccessDeniedHandlerImpl的handle方法中,处理鉴权失败的情况。如果存在错误页面就跳转到错误页面,并设置403;如果不存在错误页面则直接输出错误响应即可。

@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		if (response.isCommitted()) {
			logger.trace("Did not write to response since already committed");
			return;
		}
		if (this.errorPage == null) {
			logger.debug("Responding with 403 status code");
			response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
			return;
		}
		// Put exception into request scope (perhaps of use to a view)
		request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
		// Set the 403 status code.
		response.setStatus(HttpStatus.FORBIDDEN.value());
		// forward to error page.
		if (logger.isDebugEnabled()) {
			logger.debug(LogMessage.format("Forwarding to %s with status code 403", this.errorPage));
		}
		request.getRequestDispatcher(this.errorPage).forward(request, response);
	}

AuthenticationEntryPoint和AccessDeniedHandler都有了之后,接下 来就是Exception TranslationFilter中的处理逻辑了。

ExceptionTranslationFilter

        我们来ExceptionTranslationFilter中的doFilter方法:

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);
            //判断是否有认证失败异常(AuthenticationException)
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            //如果没有认证异常,检查是否有鉴权异常(AccessDeniedException)
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
            //不是认证异常和鉴权异常就直接抛给上一层容器去处理
			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);
			}
            //交给Spring Security去处理异常
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

        可以看到,在过滤器中直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器,然后用一个try{...}catch{...}将chain.doFilter包裹起来,如果在里面出现异常就直接在这里捕获了。

        throwableAnalyzer对象时一个异常分析器,由于异常在抛出的过程中可能被“层层转包”,我们需要还原最初的异常,通过throwableAnalyzer.determineCauseChain方法可以获取得整个异常链。举一个简单的栗子,如下面这个端代码:

public static void main(String[] args) {
        NullPointerException aaa = new NullPointerException("aaa");
        ServletException bbb = new ServletException(aaa);
        IOException ccc = new IOException(bbb);
        ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ccc);
        for (int i = 0; i < causeChain.length; i++) {
            System.out.println("causeChain[i].getClass() = "
                    + causeChain[i].getClass());
        }
    }

        打印信息如下:

causeChain[i].getClass() = class java.io.IOException
causeChain[i].getClass() = class javax.servlet.ServletException
causeChain[i].getClass() = class java.lang.NullPointerException

        所以在catch块中捕获到异常之后,首先获取异常链,然后调用throwableAnalyzer.getFirstThrowableOfType方法查询异常链中是否有认证异常AuthenticationException,不过不存在就判断时候存在鉴权异常AccessDeniedException。如果认证异常和鉴权异常都不存在就直接抛给上层容器去处理,如果存在认证异常或者鉴权异常就调用handleSpringSecurityException方法去处理。

        handleSpringSecurityException方法如下,AuthenticationException异常的话就调用handleAuthenticationException方法处理,里面是调用AuthenticationEntryPoint的commence方法处理的。AccessDeniedException异常的话就调用handleAccessDeniedException方法处理,里面是调用AccessDeniedHandlerhandle方法来处理的。

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);
		}
	}

        至此,AuthenticationEntryPoint和AccessDeniedHandler就派上作用了。

自定义异常处理

        Spring Security中 默认提供的异常不一定能满足我们的需求,如果开发者需要自定义,也是可以的,方式如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin").hasRole("admin")
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, e) -> {
                    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                    resp.getWriter().write("please login");
                })
                .accessDeniedHandler((req, resp, e) -> {
                    resp.setStatus(HttpStatus.FORBIDDEN.value());
                    resp.getWriter().write("forbidden");
                })
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

        首先我们设置了访问/admin接口必须具备admin角色,其他接口只 需要认证就可以访问。然后我们对exceptionHandling分别配置了 authenticationEntryPoint和accessDeniedHandler。上面的源码分 析,这里配置完成后,defaultEntryPointMappings和defaultDeniedHandler Mappings中的处理器就会失效。

小结

        本章主要学习了Spring Security中关于异常的处理方式。总结一下 就是在ExceptionTranslationFilter过滤器中分别对AuthenticationException 和AccessDeniedException类型的异常进行处理,如果异常不是这两种类 型的,则将异常抛出交给上层容器处理。AuthenticationException和 AccessDeniedException两种不同类型的异常,分别对应了 AuthenticationEntryPoint和AccessDeniedHandler两种不同的异常处理 器。如果系统提供的异常处理器不能满足需求,开发者也可以自定义异 常处理器,并且可以为不同的请求指定不同的异常处理器。