Spring Security源码解读:前后端分离中异常处理

本文样例代码地址: spring-security-oauth2.0-sample

关于Spring Security中OAuth2.0在前后端分离架构下的授权流程可以参考: 前后端分离:Spring Security OAuth2.0第三方授权

关于此章,官网介绍:ExceptionTranslationFilter

本文使用Spring Boot 2.7.4版本,对应Spring Security 5.7.3版本。

Introduction

在我们做登录或者越权访问时,Spring Security会根据之前的Filter抛出的异常做一些操作。如,在未登录时访问某些页面会直接重定向到登录页面。而Spring Security默认是会返回 HTML 数据,这样在前后端分离架构下是不适用的,需要另行配置。

Spring Security中提供 ExceptionTranslationFilter 处理异常,可以预料到,该Filter的执行层级会很低(责任链开始执行顺序的倒数第三,注意,开始执行顺序,实际上它处理异常的逻辑是最后执行的,参见下面代码)。该Filter会处理两类异常:

  • AuthenticationException: Abstract superclass for all exceptions related to an Authentication object being invalid for whatever reason. 负责Authentication
  • AccessDeniedException: Thrown if an Authentication object does not hold a required authority. 负责Authorization

Spring Security配置

前后端分离架构中, ExceptionTranslationFilter 对于以上异常的处理应返回application/json格式到前端,SecurityFilterChain的配置如下:

@Configuration
@EnableMethodSecurity()
@RequiredArgsConstructor
public class SecurityConfig {
    
    

	private final AccessDeniedHandler restAccessDeniedHandler;
    private final AuthenticationEntryPoint restAuthenticationEntrypoint;
	...

	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
		
		// 处理AccessDeniedException
		http.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler);
        
   		// 处理AuthenticationException
		http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntrypoint);
	}
	...    
}

来看看RestAccessDeniedHandler 和 RestAuthenticationEntrypoint:

@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
    
    
    private static final Logger logger = LoggerFactory.getLogger(RestAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
    
    
        logger.error(accessDeniedException.getMessage());
        response.setContentType(APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(JacksonUtil.getObjectMapper().writeValueAsString(ResponseEntity.status(SC_FORBIDDEN).body(accessDeniedException.getMessage())));
    }
}

--------------------------------------------------------------------------

@Component
public class RestAuthenticationEntrypoint implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
        response.setContentType(APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(JacksonUtil.getObjectMapper().writeValueAsString(ResponseEntity.status(SC_FORBIDDEN).body(Collections.singletonMap("msg", authException.getMessage()))));
    }
}

源码

ExceptionTranslationFilter

public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
    
    

	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
	// 默认实现 LoginUrlAuthenticationEntryPoint,这里重写
	private AuthenticationEntryPoint authenticationEntryPoint;
	// 异常分析器,主要从之前执行的Filter中提取	AuthenticationException 和 AccessDeniedException
	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
	...
	
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    
    
			
	
		try {
    
    
			// 先执行后面的Filter的逻辑,后面有AuthorizationFilter负责鉴权
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
    
    
			throw ex;
		}
		catch (Exception ex) {
    
    
			// 从Filter责任链调用栈中提取所有的SpringSecurityException
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
			// 先提取AuthenticationException类型异常
			RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			// 如果AuthenticationException不存在
			// 再提取AccessDeniedException类型异常
			if (securityException == null) {
    
    
				securityException = (AccessDeniedException) this.throwableAnalyzer
						.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
			// 都不存在就直接抛出
			if (securityException == null) {
    
    
				rethrow(ex);
			}
			...
			// 开始处理
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

	//分类处理
	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);
			}
		}
		
	// 该方法用作记录日志
	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
		    SecurityContext context = SecurityContextHolder.createEmptyContext();
			SecurityContextHolder.setContext(context);
			this.requestCache.saveRequest(request, response);
			// 调用RestAuthenticationEntrypoint返回json
			this.authenticationEntryPoint.commence(request, response, reason);
		}

	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登录,则返回json提示登录
		if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
    
    
		
			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(...));
		}
		else {
    
    
			// 否则直接deny
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}
	...

}

猜你喜欢

转载自blog.csdn.net/weixin_41866717/article/details/128906444