六SpringSecurity异常处理

六SpringSecurity异常处理

在 Spring Security 的过滤器链中,ExceptionTranslationFilter 过滤器专门用来处理异常。且异常分为下面两大类:

1 认证异常(authenticationException)和授权异常(AccessDeniedException)

对于认证异常:

认证异常,就是登录失败的异常。

image-20230821234752156

授权异常:(该异常出现的原因比较少)

image-20230821234848315

2 ExceptionTranslationFilter

ExceptionTranslationFilter 是 Spring Security 中专门负责处理异常的过滤器,默认情况下,这个过滤器已经被自动加载到过滤器链中。且其位于securityFilterChain上倒数第二个,最后一个为FilterSecurityInterceptor。

2.1 初始流程

使用 Spring Security 的时候,如果需要自定义实现逻辑,都是继承自 WebSecurityConfigurerAdapter 进行扩展,WebSecurityConfigurerAdapter 中本身就进行了一部分的初始化操作,我们来看下它里边 HttpSecurity 的初始化过程:

WebSecurityConfigurerAdapter
  
protected final HttpSecurity getHttp() throws Exception {
	if (http != null) {
		return http;
	}
	AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
	localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
	AuthenticationManager authenticationManager = authenticationManager();
	authenticationBuilder.parentAuthenticationManager(authenticationManager);
	Map<Class<?>, Object> sharedObjects = createSharedObjects();
  
  //初始化HttpSecurity,并进行后续的默认配置
	http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
			sharedObjects);
	if (!disableDefaults) {
    //实质:向AbstractHttpConfigurer中添加
		http
			.csrf().and()
			.addFilter(new WebAsyncManagerIntegrationFilter())
      //将 ExceptionHandlingConfigurer 配置进来,最终调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中。
			.exceptionHandling().and() 
			.headers().and()
			.sessionManagement().and()
			.securityContext().and()
			.requestCache().and()
			.anonymous().and()
			.servletApi().and()
			.apply(new DefaultLoginPageConfigurer<>()).and()
			.logout();
		ClassLoader classLoader = this.context.getClassLoader();
    
    //向HttpSecurity添加AbstractHttpConfigurer配置类
		List<AbstractHttpConfigurer> defaultHttpConfigurers =
				SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
		for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
			http.apply(configurer);
		}
	}
  
/**…………………………………………………………实现自定义WebSecurityConfigurerAdapter时实现该类,自定义需要补充的属性……………………………………………… */
	configure(http);
	return http;
}

可以看到,在 getHttp 方法的最后,调用了 configure(http);,我们在使用 Spring Security 时,自定义配置类继承自 WebSecurityConfigurerAdapter 并重写的 configure(HttpSecurity http) 方法就是在这里调用的,换句话说,当我们去配置 HttpSecurity 时,其实它已经完成了一波初始化了。

2.2 ExceptionHandlingConfigurer#configure添加对应的filter到chain

在默认的 HttpSecurity 初始化的过程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlingConfigurer 配置进来,最终调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中。

我们来看下 ExceptionHandlingConfigurer#configure 方法源码:

@Override
public void configure(H http) {
  //AuthenticationEntryPoint认证
	AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
  //构造异常处理的filter
	ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
			entryPoint, getRequestCache(http));
  //AccessDeniedHandler授权异常的处理器,并添加入filter
	AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
	exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
  
  /**SecurityConfigurerAdapter父类中,调用objectPostProcessor方法
  protected <T> T postProcess(T object) {
		return (T) this.objectPostProcessor.postProcess(object);
	}
	目的是将filter添加注册入spring的容器
	*/
	exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
  //然后将filter加入HttpSecurity中
	http.addFilter(exceptionTranslationFilter);
}

可以看到,这里构造了两个对象传入到 ExceptionTranslationFilter 中:

  • AuthenticationEntryPoint 这个用来处理认证异常。
  • AccessDeniedHandler 这个用来处理授权异常。

2.3 ExceptionTranslationFilter.doFilter异常处理逻辑(参考3.8)

public class ExceptionTranslationFilter extends GenericFilterBean {

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
       // 执行doFilter 继续执行后续过滤器 如果后续的过滤器抛出异常,也会被catch到
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;// 不处理IO异常
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
      // 先尝试获取AuthenticationException异常
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
        // 在尝试获取AccessDeniedException异常
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
        //处理异常
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}
  
  
  
  /** 1 如果检测到AuthenticationException这种认证异常,过滤器将启动authenticationEntryPoint 跳转到登录页面去认证。

			2 如果检测到AccessDeniedException,过滤器将确定用户是否是匿名用户。如果是匿名用户,authenticationEntryPoint将启动,跳转到登录页面去,如果他们不是匿名用户,过滤器将委托给AccessDeniedHandler。默认情况下,过滤器将使用AccessDeniedHandlerImpl。
*/
  private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
    //如果是AuthenticationException异常
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);

			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
    //如果是AccessDeniedException异常
		else if (exception instanceof AccessDeniedException) {
      //获取当前authentication
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      	//如果auth为匿名或者rememberMe时
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				logger.debug(
						"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
						exception);

				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
      //此时用户已经登录,但是仍然AccessDeniedException,说明无权访问
			else {
				logger.debug(
						"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
						exception);
        
        //3.8.2 accessDeniedHandler 自己实现,返回错误码、错误信息
			//调用accessDeniedHandler处理拒绝访问逻辑
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}
  
  
  
  	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		
		SecurityContextHolder.getContext().setAuthentication(null);
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
    
//3.8.1 authenticationEntryPoint 需要自己实现(跳转到login)
    //启动authenticationEntryPoint,跳转到login页面
		authenticationEntryPoint.commence(request, response, reason);
	}

ExceptionTranslationFilter 的源码比较长,我这里列出来核心的部分和大家分析:

  1. 过滤器最核心的当然是 doFilter 方法,我们就从 doFilter 方法看起。这里的 doFilter 方法中过滤器链继续向下执行,ExceptionTranslationFilter 处于 Spring Security 过滤器链的倒数第二个,最后一个是 FilterSecurityInterceptor,FilterSecurityInterceptor 专门处理授权问题,在处理授权问题时,就会发现用户未登录、未授权等,进而抛出异常,抛出的异常,最终会被 ExceptionTranslationFilter#doFilter 方法捕获。

  2. 当捕获到异常之后,接下来通过调用 throwableAnalyzer.getFirstThrowableOfType 方法来判断是认证异常还是授权异常,判断出异常类型之后,进入到 handleSpringSecurityException 方法进行处理;如果不是 Spring Security 中的异常类型,则走 ServletException 异常类型的处理逻辑。

  3. 进入到 handleSpringSecurityException 方法之后,还是根据异常类型判断,如果是认证相关的异常,就走 sendStartAuthentication 方法,最终被 authenticationEntryPoint.commence 方法处理;如果是授权相关的异常,就走 accessDeniedHandler.handle 方法进行处理。

    如果检测到AuthenticationException这种认证异常,过滤器将启动authenticationEntryPoint 跳转到登录页面去认证。
    
    如果检测到AccessDeniedException,过滤器将确定用户是否是匿名用户。如果是匿名用户,authenticationEntryPoint将启动,跳转到登录页面去,如果他们不是匿名用户,过滤器将委托给AccessDeniedHandler。默认情况下,过滤器将使用AccessDeniedHandlerImpl。
    
2.3.1 AuthenticationEntryPoint.commence(自定义拓展点)

AuthenticationEntryPoint 的默认实现类是 LoginUrlAuthenticationEntryPoint,因此默认的认证异常处理逻辑就是 LoginUrlAuthenticationEntryPoint#commence 方法,如下:

public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) throws IOException, ServletException {
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}
	else {
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

可以看到,就是重定向,重定向到登录页面(即当我们未登录就去访问一个需要登录才能访问的资源时,会自动重定向到登录页面)。

2.3.2 AccessDeniedHandler.handle(自定义拓展点)

AccessDeniedHandler 的默认实现类则是 AccessDeniedHandlerImpl,所以授权异常默认是在 AccessDeniedHandlerImpl#handle 方法中处理的:

public void handle(HttpServletRequest request, HttpServletResponse response,
		AccessDeniedException accessDeniedException) throws IOException,
		ServletException {
	if (!response.isCommitted()) {
		if (errorPage != null) {
			request.setAttribute(WebAttributes.ACCESS_DENIED_403,
					accessDeniedException);
			response.setStatus(HttpStatus.FORBIDDEN.value());
			RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
			dispatcher.forward(request, response);
		}
		else {
			response.sendError(HttpStatus.FORBIDDEN.value(),
				HttpStatus.FORBIDDEN.getReasonPhrase());
		}
	}
}

可以看到,这里就是服务端跳转返回 403。

3 自定义处理异常

首先自定义认证异常处理类和授权异常处理类:

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.getWriter().write("login failed:" + authException.getMessage());
    }
}
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(403);
        response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
    }
}

然后在 SecurityConfig 中进行配置,如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                ...
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                ...
                ...
    }
}

配置完成后,重启项目,认证异常和授权异常就会走我们自定义的逻辑了。

posted @ 2023-08-29 17:55  LeasonXue  阅读(593)  评论(0编辑  收藏  举报