本文主要簡述,Ruoyi框架使用的權限過濾實現方案,實現簡單易懂。主要知識點有:
- 注解定義;
- 面向切面編程,在執行有數據權限注解的方法之前獲取用戶組織權限,拼接到domain對象的params參數中;
1. 注解定義
ruoyi數據權限涉及部門表sys_dept和用戶表sys_users表,這里僅用來定義sql查詢中部門表和用戶表的別名。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{/*** 部門表的別名*/public String deptAlias() default "";/*** 用戶表的別名*/public String userAlias() default "";/*** 權限字符(用于多個角色匹配符合要求的權限)默認根據權限注解@ss獲取,多個權限用逗號分隔開來*/public String permission() default "";
}
2. 注解標記方法攔截后的處理
- 查看DataScopeAspect.class源碼,添加類注解:@Aspect、@Component。
- 權限類型共有五種:超管全部權限(直接跳過權限過濾)、全部數據權限、自定數據權限、部門數據權限、部門及以下數據權限、僅本人數據權限。分別對五種權限進行處理。(DataScope中定義的表別名用途就在這里)
/*** 數據過濾處理** @author ruoyi*/
@Aspect
@Component
public class DataScopeAspect
{/*** 全部數據權限*/public static final String DATA_SCOPE_ALL = "1";/*** 自定數據權限*/public static final String DATA_SCOPE_CUSTOM = "2";/*** 部門數據權限*/public static final String DATA_SCOPE_DEPT = "3";/*** 部門及以下數據權限*/public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";/*** 僅本人數據權限*/public static final String DATA_SCOPE_SELF = "5";/*** 數據權限過濾關鍵字*/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){// 獲取當前的用戶LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNotNull(loginUser)){SysUser currentUser = loginUser.getUser();// 如果是超級管理員,則不過濾數據if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()){String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),controllerDataScope.userAlias(), permission);}}}/*** 數據范圍過濾** @param joinPoint 切點* @param user 用戶* @param deptAlias 部門別名* @param userAlias 用戶別名* @param permission 權限字符*/public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission){StringBuilder sqlString = new StringBuilder();List<String> conditions = new ArrayList<String>();for (SysRole role : user.getRoles()){String dataScope = role.getDataScope();if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)){continue;}if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))){continue;}if (DATA_SCOPE_ALL.equals(dataScope)){sqlString = new StringBuilder();conditions.add(dataScope);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{// 數據權限為僅本人且沒有userAlias別名不查詢任何數據sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}}conditions.add(dataScope);}// 多角色情況下,所有角色都不包含傳遞過來的權限字符,這個時候sqlString也會為空,所以要限制一下,不查詢任何數據if (StringUtils.isEmpty(conditions)){sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}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) + ")");}}}/*** 拼接權限sql前先清空params.dataScope參數防止注入*/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, "");}}
}
在dataScopeFilter 方法最后的if方法塊中,通過向baseEntity中設置dataScope參數,將權限過濾條件放到了查詢條件中。
3. 分配數據權限
建立角色后,給角色分配數據權限,再給用戶分配一個或多個角色,這樣用戶即可獲取有權限的組織id。這里的問題即是只有支持組織權限,而且組織權限不能應用在不同場景下。
數據權限分配截圖:
4. 權限應用
- 在要添加權限的方法體上添加注解并指定部門或人員表別名;且第一個參數是BaseEntity.class類的子類,這樣才能設置dataScope查詢參數。
@DataScope(deptAlias = "d")public List<SysDept> selectDeptList(SysDept dept){return deptMapper.selectDeptList(dept);}
- sql中添加 ${dataScope}。
<select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_timefrom sys_user uleft join sys_dept d on u.dept_id = d.dept_idleft join sys_user_role ur on u.user_id = ur.user_idleft join sys_role r on r.role_id = ur.role_idwhere u.del_flag = '0' and r.role_id = #{roleId}<if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if><if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if><!-- 數據范圍過濾 -->${params.dataScope}</select>
總結:
1. 邏輯簡單,代碼實現簡潔易懂;
2. 如果還有其他表要控制權限,也可以參考該實現方法,定義注解和攔截實現權限過濾;
3. 當不同場景都要按組織授權,那么目前Ruoyi的實現不能支持;(通過第2個辦法:按場景定義不同權限注解,那還需要實現前端配置功能,和相關權限查詢功能,還是復雜)
對于企業管理類軟件,通常權限控制不止組織,很可能會按其他業務類型控制權限,那如何通過一次編碼而且實用于所有業務場景呢?這種系統有以下需求:
1. 通過配置,動態指定權限表(數據庫表)或自定義數據類型;可使程序員更關注業務邏輯,減少代碼量;
2. 而不侵入SQL代碼,僅在方法體上指定相關權限控制編碼,可指定表名或字段名;減少對業務侵入;
3. 有多種場景都是按組織分配權限,但不同場景的權限是不一樣的,在使用時不能受影響;
4. 應用多樣化,可通過方法注釋指定使用的權限類型,也可通過前端傳參指定權限類型,或者傳參指定所有權限。