1 簡介
Spring Boot中統一統一處理使用者登入權限、異常、傳回資料格式實際上都是用到了AOP。
- 異常處理使用注解: @RestControllerAdvice + @ExceptionHandler
- 使用者登入權限的校驗實作接口: HandlerInterceptor + WebMvcConfigurer
- 資料格式傳回: @ControllerAdvice 注解+ 實作接口 @ResponseBodyAdvice
2 使用者登入權限處理方案
其實作在市面上最常用的是SPring攔截器處理使用者登入異常
攔截器實作原理:
- 方案一:使用者登入權限校驗
在每個方法中擷取 Session 和 Session 中的使用者資訊,如果存在使用者,那麼就認為登入成功了,否則就登入失敗了。
該方案時最早的處理方式,每個方法中都有相同的使用者登入驗證權限.
缺點:
1、每個方法中都要單獨寫使用者登入驗證的方法,即使封裝成公共方法,也一樣要傳參調用和在方法中進行判斷
2、添加控制器越多,調用使用者登入驗證的方法也越多,這樣就增加了後期的修改成功和維護成功
3、這些使用者登入驗證的方法和現在要實作的業務幾乎沒有任何關聯,但還是要在每個方法中都要寫一遍,是以提供一個公共的 AOP 方法來進行統一的使用者登入權限驗證是非常好的解決辦法。
- 方案二:使用者登入權限校驗
提供統一的方法,在每個需要驗證的方法中調用統一的使用者登入身份校驗方法來判斷。
這種方案實際上就是把方案一的方式進行代碼抽取為公共方法,哪個方法需要的時候就直接調用。
同樣的每個方法中也要去處理一下,比較繁瑣。
- 方案三:Spring AOP統一處理使用者登入權限
下面是簡單的AOP處理使用者登入權限校驗實作案例
@Aspect // 目前類是一個切面
@Component
public class UserAspect {
// 定義切點方法 Controller 包下、子孫包下所有類的所有方法
@Pointcut("execution(* com.test.user.controller..*.*(..))")
public void pointcut(){}
// 前置通知
@Before("pointcut()")
public void doBefore() {}
// 環繞通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around 方法開始執行");
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Around 方法結束執行");
return obj;
}
}
缺點:
1、沒有辦法得到 HttpSession 和 Request 對象
2、我們要對一部分方法進行攔截,而另一部分方法不攔截,
比如注冊方法和登入方法是不攔截的,也就是實際的攔截規則很複雜,
使用簡單的 aspectJ 表達式無法滿足攔截的需求
- 方案四:Spring攔截器處理使用者登入權限校驗
針對上面代碼 Spring AOP 的問題,Spring 中提供了具體的實作攔截器:HandlerInterceptor,攔截器的實作有兩步:
1.自定義攔截器,實作 Spring 中的 HandlerInterceptor 接口中的 preHandle方法
2.将自定義攔截器加入到架構的配置中,并且設定攔截規則
- 給目前的類添加 @Configuration 注解
- 實作 WebMvcConfigurer 接口
- 重寫 addInterceptors 方法
自定義攔截器實作
/**
* @Description: 自定義使用者登入的攔截器
* @Date 2023/4/22 13:06
*/
@Component
public class LoginIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1.得到 HttpSession 對象
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 表示已經登入
return true;
}
// 執行到此代碼表示未登入,未登入就跳轉到登入頁面
response.sendRedirect("/login.html");
return false;
}
}
注冊自定義攔截器,并設定攔截器規則
/**
* @Description: 将自定義攔截器添加到系統配置中,并設定攔截的規則
* @Date 2023/4/22 13:13
*/
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Resource
private LoginIntercept loginIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(new LoginIntercept());//可以直接new 也可以屬性注入
registry.addInterceptor(loginIntercept).
addPathPatterns("/**"). // 攔截所有 url
excludePathPatterns("/user/login"). //不攔截登入注冊接口
excludePathPatterns("/user/reg").
excludePathPatterns("/login.html").
excludePathPatterns("/reg.html").
excludePathPatterns("/**/*.js").
excludePathPatterns("/**/*.css").
excludePathPatterns("/**/*.png").
excludePathPatterns("/**/*.jpg");
}
}
- addPathPatterns:表示需要攔截的 URL,**表示攔截所有⽅法
- excludePathPatterns:表示需要排除的 URL
攔截規則可以攔截此項⽬中的使⽤ URL,包括靜态⽂件(圖⽚⽂件、JS 和 CSS 等⽂件)。
3 統一異常處理
@ControllerAdvice 注解表示控制器通知類
@ExceptionHandler(xxx.class)注解表示異常處理器,添加異常傳回的業務代碼
自定義MyExceptionAdvice:
@RestControllerAdvice // 目前是針對 Controller 的通知類(增強類)
//@ControllerAdvice用這個注解也一樣的
public class MyExceptionAdvice {
@ExceptionHandler(ArithmeticException.class)
public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "算出異常:"+ e.getMessage());
return result;
}
}
空指針異常處理器:
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "空指針異常異常:"+ e.getMessage());
return result;
}
4 統一處理傳回資料格式
給目前類添加 @ControllerAdvice
實作 ResponseBodyAdvice 重寫其方法
- supports 方法,此方法表示内容是否需要重寫(通過此⽅法可以選擇性部分控制器和方法進行重寫),如果要重寫傳回 true
- beforeBodyWrite 方法,方法傳回之前調用此方法
@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {
// 傳回一個 boolean 值,true 表示傳回資料之前對資料進行重寫,也就是會進入 beforeBodyWrite 方法
// 傳回 false 表示對結果不進行任何處理,直接傳回
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
// 方法傳回之前調用此方法
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String,Object> result = new HashMap<>();
result.put("state",1);
result.put("data",body);
result.put("msg","");
return result;
}
}