Spring Security源码解读:前后端分离中异常处理
2023-09-27 14:27:14 时间
本文样例代码地址: 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. 负责AuthenticationAccessDeniedException
: 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);
}
}
...
}
相关文章
- 在Spring Boot中加载初始化数据
- Too many open files ( spring-cloud-gateway )
- XJar: Spring-Boot JAR 包加/解密工具,避免源码泄露以及反编译。
- Spring源码解析之bean加载(二)
- Spring源码前提准备
- Spring通过MimeMessageHelper发送邮件,中文附件名出现乱码解决办法
- Spring MVC的Post请求参数中文乱码的原因&处理
- Spring注解的使用和组件扫描
- spring IOC/AOP实现
- Spring Cloud 系列之Hystrix、Ribbon、Feign 源码剖析(一)引子
- 深入理解Spring源码之IOC 扩展原理BeanFactoryPostProcessor和事件监听ApplicationListener
- Spring源码分析(一)基本介绍
- Spring源码 Gradle编译Spring源码
- redis在linux下安装并測试(在spring下调用)
- 微服务与Spring Cloud基本概念、Spring Cloud版本命名方式与版本选择
- Spring MVC入门——day07
- 玩转spring boot——开篇
- web框架的前生今世--从servlet到spring mvc到spring boot
- spring cloud config配置中心源码分析之注解@EnableConfigServer
- spring源码分析之spring-core-io
- spring源码分析之定时任务概述
- spring jdbctemplate源码跟踪
- Spring源码解析之:Spring Security启动细节和工作模式--转载
- spring beans源码解读之--bean definiton解析器
- spring beans源码解读之 ioc容器之始祖--DefaultListableBeanFactory
- spring整合activeMQ