12.Spring security中的异常处理

异常处理

Spring security中关于异常的处理主要是两个方面:认证异常处理、权限异常处理,除此之外的异常则抛出,交给spring去处理。

12.1Spring Security异常体系

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

  • AuthenticationException:认证异常。
  • AccessDeniedException:权限异常。

认证异常类:

异常类型备注
AuthenticationException认证异常的父类,抽象类
BadCredentialsException登录凭证(密码)异常
InsufficientAuthenticationException登录凭证不够充分而抛出的异常
SessionAuthenticationException会话并发管理时抛出的异常,例如会话总数超出最大限制数
UsernameNotFoundException用户名不存在异常
PreAuthenticatedCredentialsNotFoundException身份预认证失败异常
ProviderNotFoundException未配置AuthenticationProvider异常
AuthenticationServiceException由于系统问题而无法处理认证请求异常
InternalAuthenticationServiceException由于系统问题而无法处理认证请求异常。和AuthenticationServiceException不同之处在于,如果外部系统出错,则不会抛出该异常
AuthenticationCredentialsNotFoundExceptionSecurityContext中不存在认证主体时抛出的异常
NonceExpiredExceptionHTTP摘要认证时随机数过期异常
RememberMeAuthenticationExceptionRemember-me认证异常
CookieTheftExceptionRemember-me认证时cookie被盗窃异常
InvalidCookieExceptionRemember-me认证时无效的cookie异常
AccountStatusException账户状态异常
LockedException账户被锁定异常
DisabledException账户被禁用异常
CredentialsExpiredException登录凭证(密码)过期异常
AccountExpiredException账户过期异常

权限异常类:

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

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

WebSecurityConfigurerAdapter#getHttp方法中进行HttpSecurity初始化的时候,就调用了exceptionHandling()方法去配置ExceptionTranslationFilter过滤器:

protected final HttpSecurity getHttp() throws Exception {
    // ...
    if (!this.disableDefaults) {
        applyDefaultConfiguration(this.http);
        // ...
    }
    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

@Override
public void configure(H http) {
    // 获取认证失败时的处理器
    AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
    // 创建ExceptionTranslationFilter过滤器并传入entryPoint
    ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
            getRequestCache(http));
    // 获取权限异常处理器
    AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
    exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
    // 注册到spring容器中
    exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
    // 添加到spring security过滤器链中
    http.addFilter(exceptionTranslationFilter);
}
AuthenticationEntryPoint

AuthenticationEntryPoint实例是通过getAuthenticationEntryPoint方法获取到的:

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
    // 默认情况下,系统的authenticationEntryPoint属性值为null
    AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
    if (entryPoint == null) {
        entryPoint = createDefaultEntryPoint(http);
    }
    return entryPoint;
}

private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
    // defaultEntryPointMappings -> LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>,
    // 即一个请求匹配器对应一个认证失败处理器,针对不同的请求,可以给出不同的认证失败处理器
    if (this.defaultEntryPointMappings.isEmpty()) {
        return new Http403ForbiddenEntryPoint();
    }
    if (this.defaultEntryPointMappings.size() == 1) {
        return this.defaultEntryPointMappings.values().iterator().next();
    }
    // 如果defaultEntryPointMappings变量中有多项,则使用DelegatingAuthenticationEntryPoint代理类,在代理类中,
    // 会遍历defaultEntryPointMappings中的每一项,查看当前请求是否满足其RequestMatcher,如果满足,则使用对应的
    // 认证失败处理器来处理
    DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
            this.defaultEntryPointMappings);
    entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
    return entryPoint;
}

当新建一个spring security项目时,不做任何配置时,在WebSecurityConfigurerAdapter#configure(HttpSecurity)方法中默认会配置表单登录和HTTP基本认证,而在这个配置过程中,会分别向defaultEntryPointMappings中添加认证失败处理器:

  • 表单登录在AbstractAuthenticationFilterConfigurer#registerAuthenticationEntryPoint方法中向defaultEntryPointMappings变量添加的处理器,对应的AuthenticationEntryPoint实例是LoginUrlAuthenticationEntryPoint,默认情况下访问需要认证才能访问的页面时,会自动跳转到登录页面,就是通过LoginUrlAuthenticationEntryPoint实现的。
  • HTTP基本认证在HttpBasicConfigurer#registerDefaultEntryPoint方法中向defaultEntryPointMappings变量添加处理器,对应的AuthenticationEntryPoint实例是BasicAuthenticationEntryPoint

所以默认情况下,defaultEntryPointMappings变量中将存在两个认证失败处理器。

AccessDeniedHandler

AccessDeniedHandler实例是通过getAccessDeniedHandler方法获取到的:

// 获取流程和AuthenticationEntryPoint基本上一模一样
AccessDeniedHandler getAccessDeniedHandler(H http) {
	AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
	if (deniedHandler == null) {
		deniedHandler = createDefaultDeniedHandler(http);
	}
	return deniedHandler;
}

private AccessDeniedHandler createDefaultDeniedHandler(H http) {
	// 不同的是,默认情况下这里的defaultDeniedHandlerMappings变量是空的,所以最终获取到的实例是AccessDeniedHandlerImpl。
	// 在AccessDeniedHandlerImpl#handle方法中处理鉴权失败的情况,如果存在错误页面,就跳转到错误页面,并设置响应码为403;
	// 如果没有错误页面,则直接给出错误响应即可
	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());
}

AuthenticationEntryPointAccessDeniedHandler都有了之后,接下来就是ExceptionTranslationFilter中的处理逻辑了。

ExceptionTranslationFilter

默认情况下,ExceptionTranslationFilter过滤器在整个spring security过滤器链中排名倒数第二,倒数第一是FilterSecurityInterceptor。在FilterSecurityInterceptor中将会对用户的身份进行校验,如果用户身份不合法,就会抛出异常,抛出来的异常,刚好就在ExceptionTranslationFilter过滤器中进行处理了:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	// 用一个try/catch块将chain.doFilter包裹起来,如果后面有异常抛出,就直接在这里捕获到了
	try {
		// 直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器(即FilterSecurityInterceptor)
		chain.doFilter(request, response);
	}
	catch (IOException ex) {
		throw ex;
	}
	catch (Exception ex) {
		// throwableAnalyzer对象是一个异常分析器,由于异常在抛出的过程中可能被层层转包,需要还原最初的异常,
		// 通过throwableAnalyzer.determineCauseChain方法可以获得整个异常链
		Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
		// 获取到异常链后调用getFirstThrowableOfType方法查看异常链中是否有认证失败类型的异常AuthenticationException,
		// 注意查找顺序,先找认证异常,再找鉴权异常,如果存在这两种类型的异常,则调用handleSpringSecurityException方法
		// 进行异常处理,否则将异常抛出交给上层容器去处理
		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()) {
			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 {
	// 判断异常类型是不是AuthenticationException,如果是,则进入handleAuthenticationException方法中处理认证失败
	if (exception instanceof AuthenticationException) {
		handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
	}
	// 如果异常类型是AccessDeniedException,则进入handleAccessDeniedException方法中处理
	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 {
	// 从SecurityContextHolder中取出当前认证主体
	Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
	// 判断是否是匿名用户
	boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
	// 如果是匿名用户,或者当前认证是通过remember-me完成的,那么也认为是认证异常,需要重新创建一个
	// InsufficientAuthenticationException类型的异常对象,然后进入sendStartAuthentication
	// 方法进行处理,否则就认为是鉴权异常,调用accessDeniedHandler.handle方法进行处理
	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 {
		this.accessDeniedHandler.handle(request, response, exception);
	}
}

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
		AuthenticationException reason) throws ServletException, IOException {
	// 清除SecurityContextHolder的认证主体,因为现有的认证不再被认为是有效的
	SecurityContext context = SecurityContextHolder.createEmptyContext();
	SecurityContextHolder.setContext(context);
	// 保存当前请求
	this.requestCache.saveRequest(request, response);
	// 调用authenticationEntryPoint.commence方法完成认证失败处理
	this.authenticationEntryPoint.commence(request, response, reason);
}
// 至此,AuthenticationEntryPoint和AccessDeniedHandler在这里就都派上用场了
12.3自定义异常配置

Spring security中默认提供的异常处理器不一定满足项目的需求,如果开发者需要自定义,也是可以的:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("javaboy")
                .password("{noop}123")
                .roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 设置访问/admin接口必须具备admin角色,其他接口只需要认证就可以访问
                .antMatchers("/admin").hasRole("admin")
                .anyRequest().authenticated()
                .and()
                // 然后对exceptionHandling分别配置了authenticationEntryPoint和accessDeniedHandler,
                // 这里配置完成后,defaultEntryPointMappings和defaultDeniedHandlerMappings中的处理器就会失效
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("please login");
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("forbidden");
                })
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

接下来启动项目,如果用户未经登陆就访问/hello接口,则:

在这里插入图片描述

当用户登录成功后,但是不具备admin角色,此时如果访问/admin接口,则:

在这里插入图片描述

当然,开发者也可以为不同的接口配置不同的异常处理器:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AntPathRequestMatcher matcher1 = new AntPathRequestMatcher("/qq/**");
        AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/wx/**");

        http.authorizeRequests()
                .antMatchers("/wx/**").hasRole("wx")
                .antMatchers("/qq/**").hasRole("qq")
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .defaultAuthenticationEntryPointFor((request, response, authException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("请登录, QQ用户");
                }, matcher1)
                .defaultAuthenticationEntryPointFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("请登录, WX用户");
                }, matcher2)
                .defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("权限不足, QQ用户");
                }, matcher1)
                .defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("权限不足, WX用户");
                }, matcher2)
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

配置完成后,不同接口将会给出不同的异常响应。

Spring Security 是一个基于 Spring 的安全框架,其支持多种认证方式、授权方式以及自定义安全过滤器等。在使用 Spring Security 进行开发时,自定义异常处理是非常重要的一部分。下面是 Spring Security 自定义异常处理的步骤: 1. 实现 AuthenticationEntryPoint 接口,该接口用于处理未认证用户的请求。通过实现该接口,可以自定义未认证用户的返回结果,例如返回自定义的 JSON 格式数据或者跳转到自定义的登录页面。 2. 实现 AccessDeniedHandler 接口,该接口用于处理已认证用户但没有权限访问资源的请求。同样可以通过实现该接口,自定义返回结果。 实现以上两个接口之后,需要在 Spring Security 配置进行配置,将自定义实现的类添加到配置即可生效。下面是一个示例配置: ``` @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(myAuthenticationEntryPoint) .accessDeniedHandler(myAccessDeniedHandler) .and() .authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() .and() .formLogin().loginPage("/login").permitAll() .and() .logout().permitAll(); } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值