本次給大家帶來的是另一個很常規但平常卻很難想到的一個設計。即在許可權控制中引入SpEL來讓復雜的許可權控制變的更簡單,更靈活。
前言
對於在Springboot中,利用自訂註解+切面來實作介面許可權的控制這個大家應該都很熟悉,也有大量的部落格來介紹整個的實作過程,整體來說思路如下:
自訂一個許可權校驗的註解,包含參數value
配置在對應的介面上
定義一個切面類,指定切點
在切入的方法體裏寫上許可權判斷的邏輯
乍一看,沒毛病,學到了,學到了~,收藏起來。但是呢,等到實際用到的時候就傻眼了,為什麽呢?在實際的開發中,你會發現,對於許可權校驗的需求場景是很多的,比如:
只要配置了任何角色,就可以存取
有某個許可權就可以存取
放行所有請求
只有超級管理員角色才可以存取
只有登入後才可以存取
在指定時間段內可以存取
有某個角色的情況下才可以存取
同時具有指定的多個角色情況下才可以存取
等
傻眼了不,按照上面的實作邏輯的話怎麽搞?加註解?寫各種判斷?這時候,其實我們就可以透過SpEL運算式來幫我們處理這個問題。
SpEL運算式
本文前面提到SpEL,那麽到底SpEL是啥呢?
SpEL的全稱為Spring Expression Language,即Spring運算式語言。是Spring3.0提供的。他最強大的功能是可以透過執行期間執行的運算式將值裝配到我們的內容或建構函式之中。
如果有小夥伴之前沒有接觸過,不太理解這句話的含義,那麽不要緊,繼續往下看,透過後續的實踐你就能明白他的作用了。
開搞
自訂註解
當然,萬變不離其宗,自訂註解我們還是需要滴。這裏呢,我們僅需要定義一個value內容用於接收運算式即可。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {
/**
*
*
* permissionAll()-----只要配置了角色就可以存取
* hasPermission("MENU.QUERY")-----有MENU.QUERY操作許可權的角色可以存取
* permitAll()-----放行所有請求
* denyAll()-----只有超級管理員角色才可存取
* hasAuth()-----只有登入後才可存取
* hasTimeAuth(1,,10)-----只有在1-10點間存取
* hasRole(‘管理員’)-----具有管理員角色的人才能存取
* hasAllRole(‘管理員’,'總工程師')-----同時具有管理員、總工程師角色的人才能存取
*
* Spring el
* 文件地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions
*/
String value();
}
定義切面
註解定義好了,我們就需要定義切面了。這裏要考慮一個點。我們希望的是如果方法上有註解,則對方法進行限制,若方法上無註解,單是類上有註解,那麽類上的許可權註解對該類下所有的介面生效。因此,我們切點的話要用@within註解。程式碼如下:
@Around(
"@annotation(PreAuth註解路徑) || " +
"@within(PreAuth註解路徑)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
if (handleAuth(point)) {
return point.proceed();
}
throw new SecureException(ResultCode.REQ_REJECT);
}
private boolean handleAuth(ProceedingJoinPoint point) {
//TODO 邏輯判斷,返回true or false
}
許可權校驗
關鍵點來了。這裏我們要引入SpEL。
首先,引入SpEL:
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
然後,從註解上獲取我們需要的運算式:
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 讀取許可權註解,優先方法上,沒有則讀取類
PreAuth preAuth = classUtil.getAnnotation(method, PreAuth. class);
// 判斷運算式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
//TODU 運算式解析
}
運算式解析
private boolean handleAuth(ProceedingJoinPoint point) {
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 讀取許可權註解,優先方法上,沒有則讀取類
PreAuth preAuth = classUtil.getAnnotation(method, PreAuth. class);
// 判斷運算式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法參數值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
//獲取解析計算的結果
return expression.getValue(context, Boolean. class);
}
returnfalse;
}
/**
* 獲取方法上的參數
*
* @param method 方法
* @param args 變量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el運算式上下文,並設定 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
// 設定運算式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < args.length; i++) {
// 讀取方法參數
MethodParameter methodParam = classUtil.getMethodParameter(method, i);
// 設定方法 參數名和值 為spel變量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
自訂解析方法
看完上面的解析處理是不是很蒙蔽,只看到了獲取運算式,獲取參數,設定參數,然後
expression.getValue
就完事了。有的同學會問,你許可權校驗的邏輯呢?
別急,關鍵點在這:
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
在上文程式碼中找到了吧。這個AuthFun就是我們進行許可權校驗的物件。
所以呢,我們還得在定義一下這個物件。進行具體的許可權校驗邏輯處理,這裏定的每一個方法都可以作為運算式在許可權註解中使用。程式碼如下:
public class AuthFun {
/**
* 判斷角色是否具有介面許可權
*
* @return {boolean}
*/
public boolean permissionAll() {
//TODO
}
/**
* 判斷角色是否具有介面許可權
*
* @param permission 許可權編號,對應選單的MENU_CODE
* @return {boolean}
*/
public boolean hasPermission(String permission) {
//TODO
}
/**
* 放行所有請求
*
* @return {boolean}
*/
public boolean permitAll() {
returntrue;
}
/**
* 只有超管角色才可存取
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授權
*
* @return {boolean}
*/
public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回異常提醒
}else{
returntrue;
}
}
/**
* 是否有時間授權
*
* @param start 開始時間
* @param end 結束時間
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判斷是否有該角色許可權
*
* @param role 單角色
* @return {boolean}
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判斷是否具有所有角色許可權
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
returnfalse;
}
}
returntrue;
}
/**
* 判斷是否有該角色許可權
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAnyRole(String... role) {
//獲取當前登入使用者
BladeUser user = AuthUtil.getUser();
if (user == null) {
returnfalse;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
returnfalse;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
returntrue;
}
}
returnfalse;
}
}
實際使用
在使用的時候,我們只需要在類上或者介面上,加上@PreAuth的直接,value值寫的時候要註意一下,value應該是我們在AuthFun類中定義的方法和參數,如我們定義了解析方法
hasAllRole(String... role)
,那麽在註解中,我們就可以這樣寫
@PreAuth("hasAllRole('角色1','角色2')")
,需要註意的是,參數要用單引號包括。
@PreAuth("hasPermission('LM_QUERY,LM_QUERY_ALL')")
public T 介面名稱....
原理
根據上面的實際使用,可以看到。SpEL運算式解析將我們註解中的"
hasAllRole('角色1','角色2')
"這樣的字串,給動態解析為了
hasAllRole(參數1,參數1)
,並呼叫我們註冊類中同名的方法。
總結
透過SpEL的使用,讓我們的許可權配置校驗更加靈活。當出現新的場景時,我們僅需要在自定的運算式解析類中增加對應場景的解析方法即可。相對於之前的實作方式,這不得不說是更好的一個選擇。
>>
END
精品資料,超贊福利,免費領
微信掃碼/長按辨識 添加【技術交流群】
群內每天分享精品學習資料
最近開發整理了一個用於速刷面試題的小程式;其中收錄了上千道常見面試題及答案(包含基礎、並行、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、訊息佇列等多個型別),歡迎您的使用。
👇👇
👇點選"閱讀原文",獲取更多資料(持續更新中)