笔者最近在努力的分析开源项目若依框架,今天看到了若依对数据权限进行控制的部分,自定义注解+AOP+动态SQL的注入,看的我是眼花缭乱,然后我又认真的复盘了一遍整个的实现过程,不由得感叹一句,若依YYDS~~
简单猜测
除了我们平时都知道的 路由权限(即对接口的访问权限外),在日常生产开发中,我们还应该有对数据的访问权限。
在若依这个框架中,通过角色中的数据范围这个属性来对数据权限进行控制。
对应实体类:
深入分析
一个用户肯定是 有一种角色的,也肯定是隶属于一个部门的。
这里咱们就以用户在查询用户时,即 selectUserList时所做的数据权限为例进行分析。
若依在进行数据权限的访问时,持久层(Mapper层)中对数据进行处理,根据用户角色的权限对数据进行过滤。
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult"> select u.user_id, u.dept_id, u.login_name, u.user_name, u.user_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.salt, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0' <if test="userId != null and userId != 0"> AND u.user_id = #{userId} </if> <if test="loginName != null and loginName != ''"> AND u.login_name like concat('%', #{loginName}, '%') </if> <if test="status != null and status != ''"> AND u.status = #{status} </if> <if test="phonenumber != null and phonenumber != ''"> AND u.phonenumber like concat('%', #{phonenumber}, '%') </if> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> AND date_format(u.create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d') </if> <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 --> AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d') </if> <if test="deptId != null and deptId != 0"> AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET (#{deptId},ancestors) )) </if> <!-- 数据范围过滤 --> ${params.dataScope}</select>
我们可以看到倒数第二行${params.dataScope}就是对数据范围进行过滤
其中,params指的是parameterType=”SysUser”传来的参数 SysUse的一个属性,然后这个属性的dataScope属性。
思考:既然params.dataScope通过占位符嵌入在这里,那么他肯定是一个sql语句。我们返回到sysUser实体类中,发现sysUser中并没有params 这个属性。
这里我们可以看到SysUser继承了BaseEntity,果然我们在BaseEntity这个类中发现了 params 这个属性
public class BaseEntity implements Serializable{ private static final long serialVersionUID = 1L; /** 搜索值 */ private String searchValue; /** 创建者 */ private String createBy; /** 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; /** 更新者 */ private String updateBy; /** 更新时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; /** 备注 */ private String remark; /** 请求参数 */ private Map<String, Object> params; public String getSearchValue() { return searchValue; } public void setSearchValue(String searchValue) { this.searchValue = searchValue; } public String getCreateBy() { return createBy; } public void setCreateBy(String createBy) { this.createBy = createBy; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public String getUpdateBy() { return updateBy; } public void setUpdateBy(String updateBy) { this.updateBy = updateBy; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public Map<String, Object> getParams() { if (params == null) { params = new HashMap<>(); } return params; } public void setParams(Map<String, Object> params) { this.params = params; }}
到这里 我们已经知道有 params这个属性的存在,并且能够肯定 params.dataScope 这是一个sql语句。 所以我们下一步就是要确定这个sql语句是什么时候注入的,怎么注入的?
高阶玩法
我们能够发现若依自定义了一个注解 @DataScope
/** * 数据权限过滤注解 * * @author ruoyi */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface DataScope{ /** * 部门表的别名 */ public String deptAlias() default ""; /** * 用户表的别名 */ public String userAlias() default "";}
我们继续探究这个注解是怎么执行的
我们可以发现在 SysUserServiceImpl Service业务层实现类中发现 @DataScope 的使用
@Override@DataScope(deptAlias = "d", userAlias = "u")public List<SysUser> selectUserList(SysUser user){ return userMapper.selectUserList(user);}
我们现在来观察DataScopeAspect 这个切面类,具体实现步骤我都在旁边进行注解了
/** * 数据过滤处理 * * @author ruoyi */@Aspect@Componentpublic 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)") // 这个controllerDataScope是一个形参 public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable { clearDataScope(point); handleDataScope(point, controllerDataScope); } protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) { // 获取当前的用户 SysUser currentUser = ShiroUtils.getSysUser(); if (currentUser != null) { // 如果是超级管理员,则不过滤数据 if (!currentUser.isAdmin()) { dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias()); } } } /** * 数据范围过滤 * * @param joinPoint 切点 * @param user 用户 * @param deptAlias 部门别名 * @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 { // 数据权限为仅本人且没有userAlias别名不查询任何数据 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) + ")"); //将完成好的sql语句放在实体类 params的 dataScope属性中 这个属性是一个Map } } } /** * 拼接权限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, ""); } }}
!!!!完结撒花!!!
如果你对这个过程看懂了的话,相信你也能够数据库中 sys_role_dept这个表的含义了吧,笔者当时也是不明白这个到底是个什么意思。其实这张表就是存放那些 自定义数据权限的角色和它对应的部门数据权限。