新聞中心
這些后置的權(quán)限處理注解有一個共同劣勢,就是得先去數(shù)據(jù)庫中查詢到數(shù)據(jù),然后再根據(jù)當(dāng)前用戶權(quán)限進(jìn)行過濾,如果數(shù)據(jù)量比較小,這樣做倒也沒有啥大問題,但是如果數(shù)據(jù)量比較大,這樣做很明顯效率會特別低,如果能在查詢的時候就把數(shù)據(jù)過濾好了,那么就會特別省事了。

成都創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供常熟網(wǎng)站建設(shè)、常熟做網(wǎng)站、常熟網(wǎng)站設(shè)計(jì)、常熟網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計(jì)與制作、常熟企業(yè)網(wǎng)站模板建站服務(wù),十年常熟做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。
巧了,松哥最近在做的 TienChin 項(xiàng)目使用了 RuoYi-Vue 腳手架,這個腳手架中自定義了一個注解 @DataScope 就是專門用來處理數(shù)據(jù)權(quán)限的,上次有小伙伴在微信上問這個注解的實(shí)現(xiàn)原理是什么樣子的?好吧,今天就安排上。
這篇文章我先和大家分析一下 RuoYi-Vue 腳手架中 @DataScope 注解的實(shí)現(xiàn)原理,在 TienChin 項(xiàng)目視頻中到時候還會有深入講解。
一、思路分析
首先我們先來捋一捋這里的權(quán)限實(shí)現(xiàn)的思路。
@DataScope 注解處理的內(nèi)容叫做數(shù)據(jù)權(quán)限,就是說你這個用戶登錄后能夠訪問哪些數(shù)據(jù)。傳統(tǒng)的做法就是根據(jù)當(dāng)前認(rèn)證用戶的 id 或者角色或者權(quán)限等信息去查詢,但是這種做法比較麻煩比較費(fèi)事,每次查詢都要寫大量 SQL,而這些 SQL 中又有大量雷同的地方,所以我們希望能夠?qū)⒅M(jìn)行統(tǒng)一處理,進(jìn)而就引出了 @DataScope 注解。
在 RuoYi-Vue 腳手架中,將用戶的數(shù)據(jù)權(quán)限分為了五類,分別如下:
- 1:這個表示全部數(shù)據(jù)權(quán)限,也就是這個用戶登錄上來之后可以訪問所有的數(shù)據(jù),一般來說只有超級管理員具備此權(quán)限。
- 2:這個表示自定義數(shù)據(jù)權(quán)限,自定義數(shù)據(jù)權(quán)限就表示根據(jù)用戶的角色查找到這個用戶可以操作哪個部門的數(shù)據(jù),以此為依據(jù)進(jìn)行數(shù)據(jù)查詢。
- 3:這個表示部門數(shù)據(jù)權(quán)限,這個簡單,就是這個用戶只能查詢到本部門的數(shù)據(jù)。
- 4:這個表示本部門及以下數(shù)據(jù)權(quán)限,這個意思就是用戶可以查詢到本部門以及本部門下面子部門的數(shù)據(jù)。
- 5:這個就表示這個用戶僅僅只能查看自己的數(shù)據(jù)。
在 TienChin 這個項(xiàng)目中,數(shù)據(jù)權(quán)限也基本上是按照這個腳手架的設(shè)計(jì)來的,我們只需要搞懂這里的實(shí)現(xiàn)思路,將來就可以隨心所欲的去自定義數(shù)據(jù)權(quán)限注解了。
二、表結(jié)構(gòu)分析
捋清楚了思路,再來看看表結(jié)構(gòu)。
這里涉及到如下幾張表:
- sys_user:用戶表
- sys_role:角色表
- sys_dept:部門表
- sys_user_role:用戶角色關(guān)聯(lián)表
- sys_role_dept:角色部門關(guān)聯(lián)表
這幾個表中有一些細(xì)節(jié)我來和大家梳理下,一個一個來看:
- 用戶表中有一個 dept_id 表示這個用戶所屬的部門 id,一個用戶屬于一個部門。
- 角色表中有一個字段叫做 data_scope,表示這個角色所對應(yīng)的數(shù)據(jù)權(quán)限,取值就是 1-5,含義就是我們上面所列出來的含義,這個很重要哦。
- 部門表在設(shè)計(jì)的時候,有一個 ancestors 字段,通過這個字段可以非常方便的查詢一個部門的子部門。
- 最后兩張關(guān)聯(lián)表就沒啥好說了。
好了,這些都分析完了,我們就來看看具體的實(shí)現(xiàn)。
三、具體實(shí)現(xiàn)
1. @DataScope
先來看數(shù)據(jù)權(quán)限注解的定義:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 部門表的別名
*/
public String deptAlias() default "";
/**
* 用戶表的別名
*/
public String userAlias() default "";
}
這個注解中有兩個屬性,一個是 deptAlias 和 userAlias。由于數(shù)據(jù)權(quán)限實(shí)現(xiàn)的核心思路就是在要執(zhí)行的 SQL 上動態(tài)追加查詢條件,那么動態(tài)追加的 SQL 必須要考慮到原本 SQL 定義時的部門表別名和用戶表別名。這兩個屬性就是用來干這事的。
所以小伙伴們可能也看出來,這個 @DataScope 跟我們以前的注解還不太一樣,以前自定義的其他注解跟業(yè)務(wù)耦合度比較小,這個 @DataScope 跟業(yè)務(wù)的耦合度則比較高,必須要看一下你業(yè)務(wù) SQL 中對于部門表和用戶表取的別名是啥,然后配置到這個注解上。
因此,@DataScope 注解不算是一個特別靈活的注解,咱們就抱著學(xué)習(xí)的態(tài)度了解下他的實(shí)現(xiàn)方式就行了。
2. 切面分析
注解定義好了,接下來就是切面分析了。
我把 RuoYi-Vue 腳手架中,解析這個注解的切面代碼列出來,咱們逐行進(jìn)行分析。
@Aspect
@Component
public class DataScopeAspect {
/**
* 全部數(shù)據(jù)權(quán)限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定數(shù)據(jù)權(quán)限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部門數(shù)據(jù)權(quán)限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部門及以下數(shù)據(jù)權(quán)限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 僅本人數(shù)據(jù)權(quán)限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 數(shù)據(jù)權(quán)限過濾關(guān)鍵字
*/
public static final String DATA_SCOPE = "dataScope";
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable {
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
// 獲取當(dāng)前的用戶
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser)) {
SysUser currentUser = loginUser.getUser();
// 如果是超級管理員,則不過濾數(shù)據(jù)
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
}
}
}
/**
* 數(shù)據(jù)范圍過濾
*
* @param joinPoint 切點(diǎn)
* @param user 用戶
* @param userAlias 別名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) {
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder();
break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) {
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
} else if (DATA_SCOPE_SELF.equals(dataScope)) {
if (StringUtils.isNotBlank(userAlias)) {
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
} else {
// 數(shù)據(jù)權(quán)限為僅本人且沒有userAlias別名不查詢?nèi)魏螖?shù)據(jù)
sqlString.append(" OR 1=0 ");
}
}
}
if (StringUtils.isNotBlank(sqlString.toString())) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
/**
* 拼接權(quán)限sql前先清空params.dataScope參數(shù)防止注入
*/
private void clearDataScope(final JoinPoint joinPoint) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, "");
}
}
}
首先一上來就定義了五種不同的數(shù)據(jù)權(quán)限類型,這五種類型咱們前面已經(jīng)介紹過了,這里就不再贅述了。
接下來的 doBefore 方法是一個前置通知。由于 @DataScope 注解是加在 service 層的方法上,所以這里使用前置通知,為方法的執(zhí)行補(bǔ)充 SQL 參數(shù),具體思路是這樣:加了數(shù)據(jù)權(quán)限注解的 service 層方法的參數(shù)必須是對象,并且這個對象必須繼承自 BaseEntity,BaseEntity 中則有一個 Map 類型的 params 屬性,我們?nèi)绻枰獮?service 層方法的執(zhí)行補(bǔ)充一句 SQL,那么就把補(bǔ)充的內(nèi)容放到這個 params 變量中,補(bǔ)充內(nèi)容的 key 就是前面聲明的 dataScope,value 則是一句 SQL。在 doBefore 方法中先執(zhí)行 clearDataScope 去清除 params 變量中已有的內(nèi)容,防止 SQL 注入(因?yàn)檫@個 params 的內(nèi)容也可以從前端傳來);然后執(zhí)行 handleDataScope 方法進(jìn)行數(shù)據(jù)權(quán)限的過濾。
在 handleDataScope 方法中,主要是查詢到當(dāng)前的用戶,然后調(diào)用 dataScopeFilter 方法進(jìn)行數(shù)據(jù)過濾,這個就是過濾的核心方法了。
由于一個用戶可能有多個角色,所以在 dataScopeFilter 方法中要先遍歷角色,不同的角色有不同的數(shù)據(jù)權(quán)限,這些不同的數(shù)據(jù)權(quán)限之間通過 OR 相連,最終生成的補(bǔ)充 SQL 的格式類似這樣 AND(xxx OR xxx OR xxx) 這樣,每一個 xxx 代表一個角色生成的過濾條件。
接下來就是根據(jù)各種不同的數(shù)據(jù)權(quán)限生成補(bǔ)充 SQL 了:如果數(shù)據(jù)權(quán)限為 1,則生成的 SQL 為空,即查詢 SQL 不添加限制條件;如果數(shù)據(jù)權(quán)限為 2,表示自定義數(shù)據(jù)權(quán)限,此時根據(jù)用戶的角色查詢出用戶的部門,生成查詢限制的 SQL;如果數(shù)據(jù)權(quán)限為 3,表示用戶的數(shù)據(jù)權(quán)限僅限于自己所在的部門,那么將用戶所屬的部門拎出來作為查詢限制;如果數(shù)據(jù)權(quán)限為 4,表示用戶的權(quán)限是自己的部門和他的子部門,那么就將用戶所屬的部門以及其子部門拎出來作為限制查詢條件;如果數(shù)據(jù)權(quán)限為 5,表示用戶的數(shù)據(jù)權(quán)限僅限于自己,即只能查看自己的數(shù)據(jù),那么就用用戶自身的 id 作為查詢的限制條件。最后,再把生成的 SQL 稍微處理下,變成 AND(xxx OR xxx OR xxx) 格式,這個就比較簡單了,就是字符串截取+字符串拼接。
好啦,大功告成,接下來我們通過幾個具體的查詢來看看這個切面的應(yīng)用。
四、案例分析
我們來看一下 @DataScope 注解三個具體的應(yīng)用大家就明白了。
在 RuoYi-Vue 腳手架中,這個注解主要有三個使用場景:
- 查詢部門。
- 查詢角色。
- 查詢用戶。
假設(shè)我現(xiàn)在以 ry 這個用戶登錄,這個用戶的角色是普通角色,普通角色的數(shù)據(jù)權(quán)限是 2,即自定義數(shù)據(jù)權(quán)限,我們就來看看這個用戶是如何查詢數(shù)據(jù)的。
我們分別來看。
1. 查詢部門
首先查詢部門的方法位于 org.javaboy.tienchin.system.service.impl.SysDeptServiceImpl#selectDeptList 位置,具體方法如下:
@Override
@DataScope(deptAlias = "d")
public ListselectDeptList(SysDept dept) {
return deptMapper.selectDeptList(dept);
}
這個參數(shù) SysDept 繼承自 BaseEntity,而 BaseEntity 中有一個 params 屬性,這個咱們前面也已經(jīng)介紹過了,不再贅述。
我們來看下這個 selectDeptList 方法對應(yīng)的 SQL:
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time
from sys_dept d
大家可以看到,在 SQL 的最后面有一句 ${params.dataScope},就是把在 DataScopeAspect 切面中拼接的 SQL 追加進(jìn)來。
所以這個 SQL 最終的形式類似下面這樣:
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time from sys_dept d where d.del_flag = '0' AND (d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 2 ) ) order by d.parent_id, d.order_num
可以看到,追加了最后面的 SQL 之后,就實(shí)現(xiàn)了數(shù)據(jù)過濾(這里是根據(jù)自定義數(shù)據(jù)權(quán)限進(jìn)行過濾)。那么這里還涉及到一個細(xì)節(jié),前面 SQL 在定義時,用的表別名是什么,我們在 @DataScope 中指定的別名就要是什么。
2. 查詢角色
首先查詢角色的方法位于 org.javaboy.tienchin.system.service.impl.SysRoleServiceImpl#selectRoleList 位置,具體方法如下:
@Override
@DataScope(deptAlias = "d")
public ListselectRoleList(SysRole role) {
return roleMapper.selectRoleList(role);
}
這個參數(shù) SysRole 繼承自 BaseEntity,而 BaseEntity 中有一個 params 屬性,這個咱們前面也已經(jīng)介紹過了,不再贅述。
我們來看下這個 selectRoleList 方法對應(yīng)的 SQL:
select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly,
r.status, r.del_flag, r.create_time, r.remark
from sys_role r
left join sys_user_role ur on ur.role_id = r.role_id
left join sys_user u on u.user_id = ur.user_id
left join sys_dept d on u.dept_id = d.dept_id
大家可以看到,在 SQL 的最后面有一句 ${params.dataScope},就是把在 DataScopeAspect 切面中拼接的 SQL 追加進(jìn)來。
所以這個 SQL 最終的形式類似下面這樣:
select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, r.status, r.del_flag, r.create_time, r.remark from sys_role r left join sys_user_role ur on ur.role_id = r.role_id left join sys_user u on u.user_id = ur.user_id left join sys_dept d on u.dept_id = d.dept_id where r.del_flag = '0' AND (d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 2 ) ) order by r.role_sort LIMIT ?
可以看到,追加了最后面的 SQL 之后,就實(shí)現(xiàn)了數(shù)據(jù)過濾(這里是根據(jù)自定義數(shù)據(jù)權(quán)限進(jìn)行過濾)。
過濾的邏輯就是根據(jù)用戶所屬的部門 id 找到用戶 id,然后根據(jù)用戶 id 找到對應(yīng)的角色 id,最后再把查詢到的角色返回。
其實(shí)我覺得查詢部門和查詢用戶進(jìn)行數(shù)據(jù)過濾,這個都好理解,當(dāng)前登錄用戶能夠操作哪些部門,能夠操作哪些用戶,這些都容易理解,能操作哪些角色該如何理解呢?特別是上面這個查詢 SQL 繞了一大圈,有的小伙伴可能會說,系統(tǒng)本來不就有一個 sys_role_dept 表嗎?這個表就是關(guān)聯(lián)角色信息和部門信息的,直接拿著用戶的部門 id 來這張表中查詢用戶能操作的角色 id 不就行行了?此言差矣!這里我覺得大家應(yīng)該這樣來理解:用戶所屬的部門這是用戶所屬的部門,用戶能操作的部門是能操作的部門,這兩個之間沒有必然聯(lián)系。sys_user 表中的 dept_id 字段是表示這個用戶所屬的部門 id,而 sys_role_dept 表中是描述某一個角色能夠操作哪些部門,這是不一樣的,把這個捋清楚了,上面的 SQL 就好懂了。
3. 查詢用戶
最后再來看查詢用戶。
查詢用戶的方法在 org.javaboy.tienchin.system.service.impl.SysUserServiceImpl#selectUserList 位置,對應(yīng)的 SQL 如下:


咨詢
建站咨詢
