深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程

Posted 金发罗婕

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程相关的知识,希望对你有一定的参考价值。

笔者最近在努力的分析开源项目若依框架,今天看到了若依对数据权限进行控制的部分,自定义注解+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') &gt;= 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') &lt;= 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)
@Documented
public @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
@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)")    // 这个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这个表的含义了吧,笔者当时也是不明白这个到底是个什么意思。其实这张表就是存放那些 自定义数据权限的角色和它对应的部门数据权限。

以上是关于深入分析若依数据权限@datascope (注解+AOP+动态sql拼接) 循序渐进,附分析过程的主要内容,如果未能解决你的问题,请参考以下文章

若依框架 使用数据权限功能

SaToken使用SpringBoot整合SaToken关于数据权限

SaToken使用SpringBoot整合SaToken关于数据权限

SaToken使用SpringBoot整合SaToken关于数据权限

RuoYi(若依开源框架)-前后台分离版-后端流程简单分析

若依vue实现动态表格,可动态控制列的显示隐藏及操作权限