Spring Security 6 系列之七 - 自定义异常管理

之所以想写这一系列,是因为之前工作过程中使用Spring Security,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0,关键是其风格和内部一些关键Filter大改,导致在配置同样功能时,多费了些手脚,因此花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。

注意由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,所有代码都在spring-security-study项目上:https://github.com/forever1986/spring-security-study.git

前面我们已经对Spring Security认证和授权做了叫深入了解,还了解了Session相关知识。但在现在的业务技术架构上面,更多使用微服务和无状态模式。这一章我们先来讲一下自定义异常处理及其前后端分离的方式。
有时候我们需要自定义异常页面或者异常返回方式(前后端分离),那么这种情况下,我们该如何做呢?我们之前在系列三中讨论过异常过滤器ExceptionTranslationFilter。这里我们先了解Spring Security的一些常见的异常,并回顾和更为深入了解其工作原理,这样我们就能清楚知道如何自定义异常处理。

1 Spring Security的异常类型

1.1 认证异常

Spring Security的认证异常接口AuthenticationException,后面的异常都是继承这个异常为主。以下列举常见的异常:

  • UsernameNotFoundException:用户名找不到,在AbstractUserDetailsAuthenticationProvider抛出,但是会被转换为BadCredentialsException异常,因为正常网站并不想泄露哪些用户名是有效的,因此不会提示用户名不存在的错误。
  • AccountStatusException:用户状态异常
  • AccountExpiredException:用户过期异常
  • BadCredentialsException:用户认证异常

在这里插入图片描述

1.2 授权异常

Spring Security的认证异常接口AccessDeniedException,后面的异常都是继承这个异常为主。以下列举常见的异常:

  • AuthorizationServiceException:授权服务器异常
  • AuthorizationDeniedException:认证拒绝异常
  • CsrfException:csrf异常
    在这里插入图片描述

2 异常处理原理

系列三中,我们初步了解到Spring Security的异常处理是由ExceptionTranslationFilter来负责。我们也对源码进行了解析,如下图所示代码

在这里插入图片描述

ExceptionTranslationFilter的doFilter方法,前面一些操作都是在识别AuthenticationException(认证异常)和AccessDeniedException(授权异常),如果不是这两个异常,则全部往外抛出。那我们看到handleSpringSecurityException方法是如何处理这2个异常的,如下图,我们可以看到是通过处理AccessDeniedException异常通过一个AccessDeniedHandler,而处理AuthenticationException是通过AuthenticationEntryPoint(这里可以看sendStartAuthentication方法具体实现)

在这里插入图片描述

那么从这里我们就知道如果要自定义异常,那么我们需要自定义AuthenticationEntryPointAccessDeniedHandler的实现类。我们也知道每个Filter过滤器都是有一个Configurer将其加入过滤器链,而ExceptionTranslationFilter就是通过ExceptionHandlingConfigurer,下图代码就是可以看到ExceptionHandlingConfigurer如何加入处理异常的Point和Handler:

在这里插入图片描述

至此,我们就知道处理Spring Security异常需要做2步:

  • 自定义AuthenticationEntryPointAccessDeniedHandler
  • 通过SecurityConfig中设置这自定义的AuthenticationEntryPointAccessDeniedHandler

3 代码示例

通过上面对其异常处理原理的分析之后,我们来使用一个代码示例演示一遍,演示内容就是将认证和授权错误信息返回自定义json格式:

代码参考lesson07子模块

1)新建lesson07子模块,其pom引入以下依赖(增加fastjson做JSON转换):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Spring Boot 提供的 Security 启动器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- 使用fastjson来转换为json-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
</dependencies>

2)增加result包,引入以下类作为统一返回工具

public interface IResultCode {
    String getCode();

    String getMsg();
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> implements Serializable {
    private String code;
    private T data;
    private String msg;
    private long total;

    public static <T> Result<T> success() {
        return success((T)null);
    }

    public static <T> Result<T> success(T data) {
        ResultCode rce = ResultCode.SUCCESS;
        if (data instanceof Boolean && Boolean.FALSE.equals(data)) {
            rce = ResultCode.SYSTEM_EXECUTION_ERROR;
        }

        return result(rce, data);
    }

    public static <T> Result<T> success(T data, Long total) {
        Result<T> result = new Result();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMsg());
        result.setData(data);
        result.setTotal(total);
        return result;
    }

    public static <T> Result<T> failed() {
        return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), (T)null);
    }

    public static <T> Result<T> failed(String msg) {
        return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), msg, (T)null);
    }

    public static <T> Result<T> judge(boolean status) {
        return status ? success() : failed();
    }

    public static <T> Result<T> failed(IResultCode resultCode) {
        return result(resultCode.getCode(), resultCode.getMsg(), (T)null);
    }

    public static <T> Result<T> failed(IResultCode resultCode, String msg) {
        return result(resultCode.getCode(), msg, (T)null);
    }

    private static <T> Result<T> result(IResultCode resultCode, T data) {
        return result(resultCode.getCode(), resultCode.getMsg(), data);
    }

    private static <T> Result<T> result(String code, String msg, T data) {
        Result<T> result = new Result();
        result.setCode(code);
        result.setData(data);
        result.setMsg(msg);
        return result;
    }

    public static boolean isSuccess(Result<?> result) {
        return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode());
    }

    public Result() {
    }

    public String getCode() {
        return this.code;
    }

    public T getData() {
        return this.data;
    }

    public String getMsg() {
        return this.msg;
    }

    public long getTotal() {
        return this.total;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public void setData(T data) {
        this.data = data;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Result)) {
            return false;
        } else {
            Result<?> other = (Result)o;
            if (!other.canEqual(this)) {
                return false;
            } else if (this.getTotal() != other.getTotal()) {
                return false;
            } else {
                label49: {
                    Object this$code = this.getCode();
                    Object other$code = other.getCode();
                    if (this$code == null) {
                        if (other$code == null) {
                            break label49;
                        }
                    } else if (this$code.equals(other$code)) {
                        break label49;
                    }

                    return false;
                }

                Object this$data = this.getData();
                Object other$data = other.getData();
                if (this$data == null) {
                    if (other$data != null) {
                        return false;
                    }
                } else if (!this$data.equals(other$data)) {
                    return false;
                }

                Object this$msg = this.getMsg();
                Object other$msg = other.getMsg();
                if (this$msg == null) {
                    if (other$msg != null) {
                        return false;
                    }
                } else if (!this$msg.equals(other$msg)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof Result;
    }

    public int hashCode() {
        int result = 1;
        long $total = this.getTotal();
        result = result * 59 + (int)($total >>> 32 ^ $total);
        Object $code = this.getCode();
        result = result * 59 + ($code == null ? 43 : $code.hashCode());
        Object $data = this.getData();
        result = result * 59 + ($data == null ? 43 : $data.hashCode());
        Object $msg = this.getMsg();
        result = result * 59 + ($msg == null ? 43 : $msg.hashCode());
        return result;
    }

    public String toString() {
        return "Result(code=" + this.getCode() + ", data=" + this.getData() + ", msg=" + this.getMsg() + ", total=" + this.getTotal() + ")";
    }
}
public enum ResultCode implements IResultCode, Serializable {
    SUCCESS("00000", "ok"),
    USER_ERROR("A0001", "用户信息为空"),
    PARAM_IS_NULL("A0410", "请求必填参数为空"),
    SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错");

    private String code;
    private String msg;

    public String getCode() {
        return this.code;
    }

    public String getMsg() {
        return this.msg;
    }

    public String toString() {
        return "{\"code\":\"" + this.code + '"' + ", \"msg\":\"" + this.msg + '"' + '}';
    }

    public static ResultCode getValue(String code) {
        ResultCode[] var1 = values();
        int var2 = var1.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            ResultCode value = var1[var3];
            if (value.getCode().equals(code)) {
                return value;
            }
        }

        return SYSTEM_EXECUTION_ERROR;
    }

    private ResultCode(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private ResultCode() {
    }
}

3)在handler包下,新建DemoAuthenticationEntryPoint(实现AuthenticationEntryPoint),用于返回认证异常处理

public class DemoAuthenticationEntryPoint implements AuthenticationEntryPoint {

    /**
     *
     * @param request 请求request
     * @param response 请求response
     * @param authException 认证的异常
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // authException.getLocalizedMessage()返回本地化语言的错误信息
        Result<String> result = Result.failed(authException.getLocalizedMessage());
        String json = JSON.toJSONString(result);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().println(json);
    }
}

4)在handler包下,新建DemoAccessDeniedHandler(实现AccessDeniedHandler),用于返回授权异常处理

public class DemoAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // accessDeniedException.getLocalizedMessage()返回本地化语言的错误信息
        Result<String> result = Result.failed(accessDeniedException.getLocalizedMessage());
        String json = JSON.toJSONString(result);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().println(json);
    }
}

5)在config包下,新建SecurityConfig,配置对应的handler

注意:这里我们返回一个test的内存用户, 并配置user角色,但是我们在配置demo接口使用的是admin角色,用户测试权限

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 所有访问都必须认证
                .authorizeHttpRequests(auth->auth
                		// demo访问接口需要admin用户
                        .requestMatchers("/demo").hasRole("admin")
                        .anyRequest().authenticated())
                // 默认配置
                .formLogin(Customizer.withDefaults())
                // 配置异常处理
                .exceptionHandling(handling -> handling
                        // 认证异常处理 -- 测试授权异常时,可以注释掉DemoAuthenticationEntryPoint
                        .authenticationEntryPoint(new DemoAuthenticationEntryPoint())
                        // 授权异常处理
                        .accessDeniedHandler(new DemoAccessDeniedHandler()));
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailsService(){
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return User.withUsername("test")
                        .password("{noop}1234")
                        .roles("user") // 给与配置用户
                        .build();
            }
        };
    }
}

6)创建一个demo的controller和启动类,并启动项目

7)访问:http://127.0.0.1:8080/demo 这时候就返回我们自定义的认证错误的json

在这里插入图片描述
8)注意:这时候我们发现,无论我们访问:http://127.0.0.1:8080/login 或其它地址都无法跳转到登录页面,这是因为当我们设置了AuthenticationEntryPoint时,就会屏蔽了DefaultLoginPageGeneratingFilter过滤器,因此不会跳转到登录界面(一般在前后端分离情况下,我们才自定义这些返回json错误)

9)测试授权异常:注释掉AuthenticationEntryPoint的设置,先访问:http://127.0.0.1:8080/login 做登录,再访问:http://127.0.0.1:8080/demo

在这里插入图片描述

结语:本章我们了解了Spring Security常见的一些异常,以及异常处理机制并自定义异常的返回。之所以要做自定义异常以Json方式返回,往往是因为前后端分离的架构,前后端一般都会约定返回Json数据格式。那么下一章,我们看看Spring Security如何做前后端分离

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

linmoo2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值