Spring Security--异常处理过滤器

文章详细分析了SpringSecurity中的ExceptionTranslationFilter如何处理AuthenticationException和AccessDeniedException。当检测到AuthenticationException时,它会调用authenticationEntryPoint进行处理,通常默认实现为LoginUrlAuthenticationEntryPoint,可自定义处理。对于AccessDeniedException,如果是匿名用户则调用authenticationEntryPoint,否则使用AccessDeniedHandler,如AccessDeniedHandlerImpl,默认行为可能重定向到错误页面,也可自定义处理返回JSON响应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ExceptionTranslationFilter过滤器处理过滤链中引发的所有AccessDeniedExceptionAuthenticationException

  • 如果检测到AuthenticationException ,则过滤器最终调用authenticationEntryPoint 处理。
  • 如果检测到AccessDeniedException ,则筛选器将确定该用户是否为匿名用户。
    • 如果是匿名用户,则调用authenticationEntryPoint 进行处理;
    • 如果不是匿名用户,则过滤器将委派给AccessDeniedHandler , 使用器实现类AccessDeniedHandlerImpl 中的方法进行处理。

1.源码分析

1.1.ExceptionTranslationFilter入口

既然是过滤器,入口必然是doFilter方法。除非写的不规范,否则一成不变。

private void doFilter(HttpServletRequest request, HttpServletResponse response, 
                          FilterChain chain) throws IOException, ServletException { 
        try { 
        	//因为只处理异常,所以上来先放行,只有catch到异常后才有真正的逻辑
            chain.doFilter(request, response);
        } catch (IOException ex) { 
            throw ex;
        } catch (Exception ex) { 
            // 尝试从异常链中获取AuthenticationException
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
            RuntimeException securityException = 
                    (AuthenticationException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            //没有AuthenticationException,再尝试获取AccessDeniedException
            if (securityException == null) { 
                securityException = (AccessDeniedException) this.throwableAnalyzer
                .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }
            // 没有AuthenticationException或者AccessDeniedException两种类型的异常,则抛出
            if (securityException == null) { 
                rethrow(ex);
            }
            // 有,则进行处理异常
            handleSpringSecurityException(request, response, chain, securityException);
        }
    }

	/** 处理SpringSecurityException异常的方法 */
    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);
        }
    }

整个入口方法的代码逻辑很清楚,不写注释也能看的明明白白。

  • 这个过滤器只处理AuthenticationExceptionAccessDeniedException两种类型的异常,其他异常直接继续上抛。
  • 两种不同类型的异常,最后会调用不同的方法进行处理。

2.AuthenticationException认证异常处理

handleAuthenticationException方法,直接调用了另一个方法sendStartAuthentication

	/** 处理方法 */
    protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException { 
        // 清除上下文中的认证信息
        SecurityContextHolder.getContext().setAuthentication(null);
        // 缓存当前请求
        this.requestCache.saveRequest(request, response);
        // 调用处理器
        this.authenticationEntryPoint.commence(request, response, reason);
    }

2.1.源码默认实现

LoginUrlAuthenticationEntryPoint 是 AuthenticationEntryPoint 的一个实现类,通过Debug我们可以看到如果出现异常,默认之进入他的处理方法中。

public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
		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);
					if (logger.isDebugEnabled()) {
						logger.debug("Server side forward to: " + loginForm);
					}
					RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
					dispatcher.forward(request, response);
					return;
				}
			}
			else {
				redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
			}
			redirectStrategy.sendRedirect(request, response, redirectUrl);
		}
	}

2.2.自定义处理

之前的文章《Spring Security-成功、失败和异常处理》里讲到过可以实现并重写authenticationEntryPoint这个类的commence方法来处理认证失败。
因为我们是前后端分离,所以需要响应json数据,前端收到响应结果后,再自行处理。

@Component
public class EdenAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException failed) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        R r = R.failed();
        if (failed instanceof InsufficientAuthenticationException) {
            r.setMsg("请先登录~~");
        } else if (failed instanceof BadCredentialsException) {
            r.setMsg("用户名或密码错误");
        } else {
            r.setMsg("用户认证失败,请检查后重试");
        }
        writer.write(new ObjectMapper().writeValueAsString(r));
        writer.flush();
        writer.close();
    }
}

3.AccessDeniedException鉴权失败处理

AccessDeniedHandler的默认实现类是AccessDeniedHandlerImpl

private void handleAccessDeniedException(HttpServletRequest request, 
                             HttpServletResponse response,FilterChain chain, AccessDeniedException exception) throws ServletException, IOException { 
        // 获取上下文中的authentication认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 判断是否匿名
        boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
        // 匿名用户或者设置了rememberMe,由AuthenticationException进行处理
        if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) { 
        	// 这个方法就是认证失败异常处理里面的sendStartAuthentication方法
            sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(xxx));
        } else { 
            // 核心处理方法
            this.accessDeniedHandler.handle(request, response, exception);
        }
    }

3.1.源码默认实现

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 {
				// 没有错误页就返回JSON
				response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
			}
        }
    }

我们肯定没有错误页,而且看下面默认返回的json数据也不符合我们的要求:

{
    "timestamp": "2023-02-12T01:44:28.522+00:00",
    "status": 403,
    "error": "Forbidden",
    "message": "",
    "path": "/oauth/toLogin"
}

3.2.自定义处理

自定义实现AccessDeniedHandler并重写handle方法。

@Component
public class EdenAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        if (!response.isCommitted()) {
            WebUtils.response(response, HttpServletResponse.SC_FORBIDDEN,
                    new ObjectMapper().writeValueAsString(R.failed("权限不足")));
        }
    }
}

上面写的是WebUtils.response方法就是自定义了一个工具类,其实和认证失败里面的代码几乎一样的,就是返回的信息不同,我就不再挪过来了,自己看上面怎么写的即可。

4.Security配置

自定义的方法写完了,还需要配置到Security的配置里面,否则不生效。

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        String ignoreUrls = "";
        if (CollectionUtil.isNotEmpty(authProperty.getIgnoreUrls())) {
            ignoreUrls = String.join(StrUtil.COMMA, authProperty.getIgnoreUrls());
            http.authorizeRequests().antMatchers(ignoreUrls).permitAll();
        }
        http
                // 省略无关配置
                .exceptionHandling()
                .authenticationEntryPoint(entryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值