之所以想写这一系列,是因为之前工作过程中使用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方法具体实现)

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

至此,我们就知道处理Spring Security异常需要做2步:
- 自定义AuthenticationEntryPoint和AccessDeniedHandler
- 通过SecurityConfig中设置这自定义的AuthenticationEntryPoint和AccessDeniedHandler
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如何做前后端分离
6003

被折叠的 条评论
为什么被折叠?



