Spring Security异常处理

本文内容来自王松老师的《深入浅出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 不同之处在于,如果外部系统出错,则不会抛出该异常
AuthenticationCredentialsNotFoundExceptionSecurityContext中不存在认证主体时抛出的异常
NonceExpiredExceptionHTTP摘要认证时随机数过期异常
RememberMeAuthenticationExceptionRememberMe认证异常
CookieTheftExceptionRememberMe认证时Cookie 被盗窃异常
InvalidCookieExceptionRememberMe认证时无效的Cookie异常
AccountStatusException账户状态异常
LockedException账户被锁定异常
DisabledException账户被禁用异常
CredentialsExpiredException登录凭证(密码)过期异常
AccountExpiredException账户过期异常

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

Spring Security权限异常类
异常类型备注
AccessDeniedException权限异常的父类
AuthorizationServiceException由于系统问题而无法处理权限时抛出异常
CsrfExceptionCsrf令牌异常
MissingCsrfTokenExceptionCsrf令牌缺失异常
InvalidCsrfTokenExceptionCsrf令牌无效异常

        实际项目中,如果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;
	}

        默认情况下authenticationEntryPoint属性为null,所以最终会通过createDefaultEntryPoint方法区来获取AuthenticationEntryPoint的事例。在createDefaultEntryPoint方法中,有一个defaultEntryPointMappings实例,它是一个 LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>类型。其的key是一个RequestMatcher,即一个请求适配器,而value是一个AuthenticationEntryPoint认证失败处理器,即一个请求匹配一个认证失败处理器。换句话说,针对不同的请求,可以给出不同的认证失败的处理器。如果defaultEntryPointMappings变量为空的话,则返回一个Http403ForbiddenEntryPoint类型的认证失败处理器,如果只有一项就直接取出来,如果存在多项的话就使用代理类DelegatingAuthenticationEntryPoint,在代理类中会遍历defaultEntryPointMappings变量中的每一项,查看当前的请求是否满足其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两种不同的异常处理 器。如果系统提供的异常处理器不能满足需求,开发者也可以自定义异 常处理器,并且可以为不同的请求指定不同的异常处理器。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security提供了许多方式来处理身份验证和授权的异常。下面是一些常见的异常处理方式: 1. 使用Spring的全局异常处理器:可以通过在应用程序中定义一个`@ControllerAdvice`类,并使用`@ExceptionHandler`注解来处理Spring Security的异常。在异常处理方法中,可以根据不同的异常类型进行相应的处理,例如返回自定义的错误页面或者JSON响应。 ```java @ControllerAdvice public class SecurityExceptionHandler { @ExceptionHandler(AuthenticationException.class) public ResponseEntity<String> handleAuthenticationException(AuthenticationException ex) { // 处理身份验证异常 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<String> handleAccessDeniedException(AccessDeniedException ex) { // 处理访问拒绝异常 return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); } // 其他异常处理方法... } ``` 2. 自定义AccessDeniedHandler:可以实现`AccessDeniedHandler`接口来处理访问拒绝异常。可以在`handle()`方法中自定义处理逻辑,例如返回自定义的错误页面或者JSON响应。 ```java public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { // 处理访问拒绝异常 response.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage()); } } ``` 然后,在Spring Security配置中指定自定义的`AccessDeniedHandler`: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // ... @Override protected void configure(HttpSecurity http) throws Exception { http // ... .exceptionHandling() .accessDeniedHandler(new CustomAccessDeniedHandler()) // ... } // ... } ``` 这些只是处理Spring Security异常的两种常见方式,你可以根据实际需求选择适合的方式进行异常处理
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值