一文搞定 Spring Security 异常处理机制!

今天来和小伙伴们聊一聊 Spring Security 中的失常处理机制。

在 Spring Security 的过滤器链中,ExceptionTranslationFilter 过滤器专门用来处理失常,在 ExceptionTranslationc [ O 7 zFilter 中,我们可以看到,失常被分为了两大类:认证失常和授权失常,两种失常分别由不同的回调函数来处理,今天松哥就来和我们共享一下这儿的条条框框。

1.失常分类

Spring SecurX I D 4 – eity 中的失常可以分为两4 5 #大类,一种是认证失常,一种是授权失常。

认证失常就是 AuthenticationException,它有许多的完结类:

一文搞定 Spring Security 失常处理机制!

可以看到,这儿的失常完结类{ 3 V仍是蛮多: X 5 h ( : ~ P的,都是都h c G Z是认证相关的失常,也就是登录失利的失常。这些失常,有的松哥在之前的文章中都和我们介绍过了,例如下面这段代码(节选自:Spring Sm . Q c 7 @ 6 C pecurity 做前后端_ 1 ~ ( – H分离,0 o v 8 s $ Y C b咱就别做页面跳转了!通通 JSON 交互):

resp.setContentType("H : x t G Happlicat 6 f Gion/json;c1 % 8 K = )  Mharset=utf-8") V g z ; J d;
PrintWriter out = resp.getWrit, Q | C g d per();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanB m q Rceof LockedException) {
respBv 1 3ean.setMsg("账户被确S 0 k ^ } j l V定,请联络管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("暗码过期,请联络管理员!");
} else if (e instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联络管理员!");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联络管理员!9 * v [ V ? Y L");
} else if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或许暗码输入差错,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();

另一类就是授权失常 AccessDeniedException,授权失常的完结类比较少,由于授权失利的或许原因比较少。

一文搞定 Spring Security 失常处理机制!

2.ExceptionTranslationFilter

ExceptionTranslationFilter 是 Spring Security 中专门负责处理失常的过滤器,默许情况下,这个过~ + M 7 H , | n L滤器现已被自动加载到过滤器链中。

有的小伙伴或许不{ J Z Z y P ; ~ 8清楚是怎样被加载的,我这儿和我们稍微说一下。

当我们运用 Spring Security 的时分,假如需要自定义完结逻辑,都是继承自 WebSecurityConfigurerAdapter 进行扩展,WebSecurityConfigurerAdapter 中自身就进行了一部分的初始化操作,我们来看下它里面 HttpSecurity 的初始化进程:

protected final HttpSecurity getHttp() thro[ U p ^ u Sws Ex1 3 P ^ception {
if (http != null) {
return ht8 3 7 y o Z Atp;
}
AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
localCo0 B U x I GnfigureAu0 F m _ ? ~ ( G 3thenticationBldr.authenQ W - r y m O %ticaR 6 w f D vtionEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = authenticationManager(& R d D);
authenticationBuilder.parentAuthenticat] m X 2ionManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
http = new HttpSecurity(objectPostProcessor, authenticationBuil= G B a @der,
sharT l $ z &edObjects);
if (!disableDefaults) {
http
.csrf().and()
.addF+ = [ u ] ilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionMan4 ; ) D C , bagemen8 g 2 D x +t().and()
.s - / a 7 ^ 0ecurityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoge g A % F /inPageConfig( 7 e h Q E 0 T =urer<>()).and()
.logout();
ClassLoade# ? ` ] d ;r classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return hH } 2 , qttp;
}

可以看到,在 getHttp 方法的毕竟,调用了 configure(http);,我们在运用 S@ Y ( @ # jpring Security 时,自定义配备类继承自 WebSecurityConfigurerAdapter 并重写的 configure(HttpSecurity http) 方法就是在这儿调用的,换句话说,当我们去装Q Z – ]备 HttpSecurity 时,其实它现已完结了一波初始/ * ( 9 [ =化了。

在默许+ E k U – k N的 HttpSecurity 初始化的进程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlinA ! VgConfigurer 配备进来,毕竟调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中。S 5 / }

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

@Override
public void configure(H http) {
AuthenticationEntryPoint entryPoint = gete I 1 ( B i ]AuthenI r 4 g ; jticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
entryPoiK Y H vnt, getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandleR $ / Y h Y {r(http);
exceptionTranslationFilter.setAccessDeniedHandler(a ! M Y D , RdeniedHandler);
exceptionTranslationFiltn  I ^ O Xer = pG D 9 D u  $ N RostProcess(6 B ! : % V vexceL ) YptionTranslationFilter);
http.addFilter(e5 0 ~ 5 rxceptionTranslationFilter);
}

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

  • Authl { z ^ genticaQ } Q KtionEntryPoint 这个用来处理认证失常。
  • AccessDeniedHandler 这个用来处理授权失常。

具体的处理逻辑则, z i O b : ^在 ExceptionTranslationFilter 中,我们来看一下:

pu@ - q V Y * J W ;blic class ExceptionTranslationFilter extends GenericFilterBean {
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
Requ! X [ T F ; T U testCache requestCache) {
this.au: ? Tthentiv * R RcationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
public void doFilter(ServletRequest req, Seq N : ` JrvletResponse res, FilterChain chain)
thrb 5 ) rowl g J 8 % & A 7s IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponseh = 4 w _ 3  W response = (HttpServletResponse) res;
try {
chain.doFiltY 7 : {er(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex)6 & M b X {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) tT b / ihrowableAnalyzer
.getFirstThrowableOfType(Authenticatio6 % 9 B H 9nException.class, caG s * $ duseChain);
if (ase == null) {
ase = (AccY 0 A 9 p t G ? 8essDeniedException) throwab! P n o g l c ( IleAnalyzer.getFirstThrowableOfType(
AccessDeniedExcX 5 aeption.class, causeChaiw @ T Un);
}
if (ase != null) {
if (response.isComN X # s I -mitted())h ( c G } H # ~ t {
throq ( G - 5 E q |w new ServletException("Unable to handle the Spring Security Exce- N P p t M Yption because th6 ( 2 ` Ie response is already committed.", ex);
}
handleSpringS0 7 - / WecurityException(request, response, chain, ase);
}
else {
if (ex instanceof ServletExceptiony p 9 U ^ b) {
throw (ServletException) ex;
}
else if (ex insX } j r A l ( Utanceof RuntimeException) {
throw (RuntimeExcv 3 Feption1 } ,) ex;
}
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IO| ] . O C g z { vException, S! i _ R CervletException {
if (exception instanceof AuthenticationException) {
sendStartAuthentication(request, resp8 i e u ^onse, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.o / ~ 1 { 5 ? IisAnonymous(authentication) || authenticationTrustResolver.isRemX 8 RemberMH 8 & } U Qe(authen5 h Q * A ^ C E =tication)) {
sendSta{ / 3 | ( + #rtAuthentication(
request,
response,
chain,
new InsufficientAuthenticatios T * R e + l + enException(
mes]  Ysages.getMessage(
"c f LExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access th] n 7 L V } [is resource")));
}
else {
accessDeniedHandler.handT W w 4le(request, response,
(AccessDeniedv a 7 wException) exceptM 5 0 J , w hion);
}
}
}
protected void sendStartAuthentU [ l 1 e oication(HttpServletRe| ) / J O u + T hquest request,
HttpSZ D i 2 { j ervletResponse respoV c Snse, FilterC$ D m K Y : bhai- z 6 e &n chain,
AuthenticationException reason) throws ServletException, IOExceptionF 6 ! 2 a R L H {
SecurityCon: b & S N 2 /textHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.deb_ v ;ug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
}

ExceptionTranslationFilter 的源码比较长,我这儿列出来中心的部分和我们剖析:

  1. 过滤器最中心的当然是 doFilW & B ` D W V | nter 方法,我们就从 doFilter 方法看起4 e p , 6 A W Z。这儿的 doFilter 方法中过滤器链继续向下实行,ExceptionTranslationFilter 处于 Spring Security 过% v k @ M ! s g滤器链的倒数第二个,毕竟一个是 FilterSv = E ! | ,ecurityInterceptor,Filter% Z 8 J c R ! lSecurityInterceptor 专门处理授权问题) F $ S + , b ) ,在处理授权问题时,就会发现用户未登录、未授权等,从而抛出失常,抛出的失常,毕竟会被 ExceptionTransl^ P w ) Q 1 H ,ationFilter#doFilter 方法捕获。
  2. 当捕获到失常之后,接下来通过调用 throwableAnalyzer.getFirstThrowe $ SableOfd d , | :Type 方法来判别是认证失常仍是授权失常,判别出失常类型之后,进入到 handleSpringSecurityException 方法进行处! t !理;假如不是 Spring Security 中的失常+ C S A 1 L类型,则走 ServletExceptioq ` & 2n 失常类型的处理逻辑。
  3. 进入到 handleSpringSecurityException 方法之后,仍是根据失常类型判别,假如是认证相关的失常,就走 sendStartAuthentication 方法,毕竟被 authentic~ & T % q c Z EationEntryPoint.commence 方法处理;O B 4 f 6 V假如是授权相关的失常,就走 accessDeniedHanb 5 F ?dler.handle 方法进行处理。

AuthenticatiH 3 F s 0 * qonEntrq ! & V f _ @ gyPoint 的默许完结类是 Logd { L E 7 5 E CinUrlAuF Q q xthenticationEntryPoint,因而默许的认证失常处理逻辑就是 LoginUrlAuthenticationEntryPoint#commence 方法,如下:

public void commence(HttpServletRequest request, Ht) x v Q u b YtpServletResponse response,
Authenticatc W T + 6 1 v , tionException authException) throws IOException, ServletException {
String redireck 5 RtUrl = null;
if (useForward) {, { 6 f  0 N e
if (foM 7 7rceHttps && "http".equals(request.getScheme())) {
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if+ k U A n T M (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, re s _ Z  V H 3 )esponse,
authEc * t Y J * 4 @ 1xception);
RequestDispatcher dispatcher = request.getRequestDispatcher(loS I $ i N X 1 (ginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buiy U N p b 2ldRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}

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

AccessDenB | e K O BiedHand1 h –ler 的默许完结类则是 AccessDeniedHandlerImpl,所以授权失常默许是在 AccessDeniedHandlerImpl#handle 方法中处理的:

public void handle(Ht2 t t a # 5 / B 7tpServletRequest reqC I H [ r z 2uest,U . m Htr ) u j |  ` AtpServletResponse response,
AccessDeniedException accessDeniedExcep~ T j dtion) throws IOException,
Sy A + J nervletException {
if (!response  T ^.isCommitted()) {
if (errorPage != null) {
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
response.setStat& C G M | g Y ` ius(Hj [ p i & N WttpStatus.FORBIDDEN.value());
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request; I e ^ % q, response);
}
else {
response.sendError(0 k d THttpStax ` V 3 M +tus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasoq  S = N 6 6nPhrase());
}
}
}

可以看到,] ) { Q 6 S a E这儿就是服务端跳转返回 403。

3.自定义处理

前面和我们介绍了 Spring Security 中默许的处理逻辑,实践开发中,我们可以需要做一些调整,很简单,在 exceptionHandling 进步行配备即可。

首要自定义n 5 F }认证失常处理类和授权失常处理类:

@Component
public class MyAuto d o / i J  U DhenticationEn| q F M G K KtryPoint implement K F q U k C s AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletRg ; } l ; $ ) d gesponK S & k R 4 sse response, AuthenticationException authException; 1 } q) throk 3 u 5 7ws IOException, ServletException {
response.getWriter().write("6 } | ~ a c )login failed:" + authException.getj 0 O B HMessage())M 4 K - T + :;
}
}
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequeV f | o ? = ast request, HttpServletResponse response, AccessDeniedException acceM m i $ 8 XssDeniedException)Z g u throws IOException,? r C M w u a ServletException {
response.setStatus(403);
response.g% , C #etWriter().wri+ - ^ ? 6 3 y ite("Forbidden:" + accessDeniedException.getMessage());
}@ i { K I N K  g
}

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

@Configuration
public claj I , - [ , Kss SecurityConfig extends WebSecurityConfigurerAdapt* u B E O @ 2 j 3er {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authY q s G ? 5 dorizeRequests()
...
...
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryP` | d L goint)
.accessDeniedHandler(myAccessDenie1 a 3 - `dHandler)
.and()
...
...
}
}

配备完结后,重启项; b z H B O l : g目,认证失常和授权失常就会走我们自定义的逻辑了。

4.小结

好啦,今天主要和小伙伴们共享了 Spring Security 中的失常处理机制,感兴趣的小伙伴可以试一试哦~

文中代码下载地址:github.com/lenve/sprin…

大众号【江南一点雨】后台回复 springsecurity,获取Sprin8 n r Lg Security系列 40+ 篇完整文章~