Spring Security的异常处理

629 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情

Spring Security的异常处理

\qquad异常也算是一个开发中不可避免的问题,Spring Security中关于异常的处理主要是两个方面:认证异常处理、权限异常处理。除此之外的异常则抛出,交给Spring 去处理。

Spring Security 异常体系

\qquadSpring Security中的异常主要分为两大类:

\qquad●AuthenticationException: 认证异常。
\qquad●AccessDeniedException: 权限异常。

\qquad其中认证异常涉及的异常类型比较多,举几个例子,也是我们经常会碰到的(异常提示信息)

UsemameNotFoundException:用户名不存在异常
ProviderNotFoundException:未配置AuthenticationProvider异常。
AuthenticationServiceException:由于系统问题而无法处理认证请求异常。
AuthenticationCredentialsNotFoundException:SecunityContext 中不存在认证主体时抛出的异常
AccountStatusException:账户状态异常
LockedException:账户被锁定异常
DisabledException:账户被禁用异常
CredentialsExpiredException:登录凭证(密码)过期异常
AccountExpiredException:账户过期异常,等等...

\qquad由于授权流程比认证流程简单,所以说相比于认证异常,权限异常类就显得比较少:

AccessDeniedException:权限异常的父类.
AuthorizationServiceException:由于系统问题而无法处理权限时抛出异常
CsrfException:Csrf令牌异常
MissingCstrfTokenException:Csrf令牌缺失异常
InvalidCsrfTokenException:Csrf令牌无效异常,等等...

\qquad虽然Spring Security提供了诸多的异常处理类,但是在实际项目中,某些场景下Spring Security所提供的异常类无法满足需求,那么我们也可以自定义异常类,来满足我们的需要。在前面的小demo里我们曾经自定义了登录失败的异常信息,用来提示。在Spring Security里面也有专门的过滤器——ExceptionTranslationFilter来处理这些异常。

ExceptionTranslationFilter 原理分析

\qquadSpring Security中的异常处理主要是在ExceptionTranslationFilter 过滤器中完成的,该过滤器主要处理AuthenticationException(认证)和AccessDeniedException(授权)类型的异常,其他异常则会继续抛出被上一层容器捕获,进而对这些异常进行处理。
\qquad在之前的demo里我们在配置类的configure(HttpSecurity http)方法中,对HtpSecurity初始化的时候就调用了exceptionHandling()方法,去配置ExceptionTranslationFilter过滤器:

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .mvcMatchers("/vc.jpg").permitAll()
            .mvcMatchers("/admin").hasRole("admin")
            .mvcMatchers("/dba").hasRole("dba")
            .mvcMatchers("/user").hasRole("user")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .exceptionHandling() //开启对ExceptionTranslationFilter过滤器的配置
            .authenticationEntryPoint(((request, response, authException) -> {
                Map<String,Object> result = new HashMap<>();
                result.put("msg","您还未进行登录!");
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }))
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(((request, response, authentication) -> {
                Map<String,Object> result = new HashMap<>();
                result.put("msg","注销登录");
                result.put("userInfo",authentication.getPrincipal());
                response.setStatus(HttpStatus.OK.value());
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().println(s);
            }))
            .and()
            .csrf().disable();
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

\qquadexceptionHandling()方法就是调用ExceptionHandlingConfigurer去配置ExceptionTranslationFilter。对ExceptionHandlingConfigurer 配置类而言,最重要的当然就是它里边的configure方法,其源码如下:

public void configure(H http) {
    AuthenticationEntryPoint entryPoint = this.getAuthenticationEntryPoint(http);
    ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, this.getRequestCache(http));
    AccessDeniedHandler deniedHandler = this.getAccessDeniedHandler(http);
    exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
    exceptionTranslationFilter = (ExceptionTranslationFilter)this.postProcess(exceptionTranslationFilter);
    http.addFilter(exceptionTranslationFilter);
}

\qquad可以看到,这里首先获取到一个entryPoint对象(也是我们自己配置的),这个就是认证失败时的处理器,然后创建ExceptionTranslationFilter过滤器的对象时传入entryPoint对象, 其次,调用本类的getAccessDeniedHandler(http)方法获取到一个deniedHandler对象(授权的处理)设置给ExceptionTranslationFilter过滤器,这个处理对象就是权限异常处理器。最后调用postProcess 方法将ExceptionTranslationFilter 过滤器注册到Spring 容器中,然后调用addFilter方法再将其添加到Spring Security过滤器链中。
\qquad在这个流程中getAuthenticationEntryPoint(http)获取的是对认证流程过程中异常的处理,而getAccessDeniedHandler(http)就是对授权过程中异常的处理:

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
    AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
    if (entryPoint == null) {
        entryPoint = this.createDefaultEntryPoint(http);
    }

    return entryPoint;
}

private AccessDeniedHandler createDefaultDeniedHandler(H http) {
    if (this.defaultDeniedHandlerMappings.isEmpty()) {
        return new AccessDeniedHandlerImpl();
    } else {
        return (AccessDeniedHandler)(this.defaultDeniedHandlerMappings.size() == 1 ? (AccessDeniedHandler)this.defaultDeniedHandlerMappings.values().iterator().next() : new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings, new AccessDeniedHandlerImpl()));
    }
}

private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
    if (this.defaultEntryPointMappings.isEmpty()) {
        return new Http403ForbiddenEntryPoint();
    } else if (this.defaultEntryPointMappings.size() == 1) {
        return (AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next();
    } else {
        DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(this.defaultEntryPointMappings);
        entryPoint.setDefaultEntryPoint((AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next());
        return entryPoint;
    }
}

\qquad从代码上可以看出, AccessDeniedHandler实例的获取流程和AuthenticationEntryPoint的获取流程是差不多的,这里都有一个defaultDeniedHandlerMappings变量,它可以为不同的路径配置不同的认证或者鉴权失败的处理器,并且如果这些存在的认证和鉴权处理器都失败了,也可以由代理类进行统一处理。