若依系统权限认证过程详解


简介

​ 由于若依的权限认证是自研开发的,所以总有小伙伴表示若依的权限认证实现过程看不懂,不清楚认证流程,再加上上周权限部分又进行了一次更新。所以我这里基于新版的权限认证详细的带大家学习一下实现过程

由于项目还在不断的更新升级过程中,对相应代码可能存在再次更新调整的可能,具体代码请根据现有版本代码自行判断

本文章基于RuoYi-Cloud v3.2.0版本

前言

其实有很多人在问,框架中为什么没有使用Spring Security OAuth 2.0框架?

这个问题其实是一个无奈的历史问题,其实我们在Git提交记录上可以看到,早期的若依版本其实使用了Spring Security OAuth 2.0框架的,但是在一次更新时移除了OAuth 2.0

Git版本更新说明

原因是因为若依的作者早期使用的是PigCloud框架中的OAuth 2.0实现,但是后来与PigCloud的作者对源码授权产生了纠纷,被指控不遵守LGPL3.0协议,商业版本,违规、盗版使用。作者一气之下就移除了OAuth2全部权限代码

作者在gitee上的说明

事件发展过程+双方陈述:https://gitee.com/y_project/RuoYi-Cloud/issues/I1TIJO

具体过程我也不甚清楚,也不再进行过多讨论

也希望大家遵从作者的意愿,不讨论、不吃瓜

权限实现

在框架中的权限我姑且将其分为三个部分:接口认证权限、数据认证权限和内部认证权限

其中接口认证权限使用注解@RequiresPermissions、@RequiresLogin和@RequiresRoles实现,数据认证权限使用注解@DataScope实现,内部认证权限使用@InnerAuth实现

注:早期接口权限使用的注解为@PreAuthorize,作者于2021.10.16进行了一次优化,将其拆分为RequiresLogin、RequiresRoles、RequiresPermissions,并将所有的@PreAuthorize注解替换为了@RequiresPermissions注解。

如果你使用的是较早的版本,可以简单理解为@PreAuthorize = @RequiresPermissions

Git更新日志

然后我们通过代码的注释可以看出,@RequiresLogin、@RequiresPermissions和@RequiresRoles分别对应的是登录认证、权限认证和角色认证

/**
 * 登录认证:只有登录之后才能进入该方法
 * 
 * @author ruoyi
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresLogin
{
}
/**
 * 权限认证:必须具有指定权限才能进入该方法
 * 
 * @author ruoyi
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresPermissions
{
    /**
     * 需要校验的权限码
     */
    String[] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     */
    Logical logical() default Logical.AND;
}
/**
 * 角色认证:必须具有指定角色标识才能进入该方法
 * 
 * @author ruoyi
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles
{
    /**
     * 需要校验的角色标识
     */
    String[] value() default {};

    /**
     * 验证逻辑:AND | OR,默认AND
     */
    Logical logical() default Logical.AND;
}

可以看出作者对接口权限进行了更细致的划分,但是目前系统中并没有使用@RequiresLogin和@RequiresRoles,应该是为我们二次开发预留的认证方式

在数据认证权限方面,框架中将数据认证权限中划分为以下5个等级

com.ruoyi.common.datascope.aspect.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"

数据权限是与业务流程强耦合的,并非所有查询都能适用,必须满足一定条件(即查询结果必须存在依赖项,双方有关联)

不满足耦合条件的查询,只能设置为全部数据权限

  • 自定数据权限、部门数据权限、部门及以下数据权限 依赖与部门ID

  • 仅本人数据权限 依赖于用户ID

  • 全部数据权限 无依赖项

总结:框架中的权限认证使用基于 Spring Aop 的注解鉴权,包含了5种注解实现

  • 接口认证权限:@RequiresPermissions、@RequiresLogin、@RequiresRoles

  • 数据认证权限:@DataScope

  • 内部认证权限:@InnerAuth

接口认证权限

由于在系统中新增的注解@RequiresLogin和@RequiresRoles目前没有使用,所以暂时不做讨论,我们只来聊一聊@RequiresPermissions

我们来进行一次完整的请求访问过程模拟

  1. 在系统管理中为用户赋予角色,为角色赋予权限

  2. 在Controller中,通过@RequiresPermissions为接口方法声明所属权限

    @RequiresPermissions的使用方式

    登录后我们可以在Redis中看到所存放的用户信息

    Redis中的用户信息

    通过Redis中的数据可以看出,我刚刚为一名用户赋予了leader的角色,角色中包含用户管理的新增、删除、修改和查询等接口权限

认证过程:

  1. Gateway网关登录鉴权

    前端发送的http请求进入Gateway网关模块,首先进入黑名单、验证码和缓存请求等几个过滤器进行一些数据处理,随后进入com.ruoyi.gateway.filter.AuthFilter全局过滤器,进行登录鉴权

    全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。目前网关统一鉴权AuthFilter.java就是采用的全局过滤器。

    单独定义只需要实现GlobalFilter, Ordered这两个接口就可以了。

    AuthFilter登录鉴权这里主要完成三件事

    • 判断请求目标是否在白名单内

    • 验证token是否有效

    • 将一部分用户信息添加到Header中

    AuthFilter过滤器代码

    进入到过滤器中,首先验证请求目标地址是否符合白名单内的匹配规则(白名单配置),匹配规则如下

    白名单匹配规则

    通过匹配(在白名单内)则通过登录鉴权,接口无需登录既可访问,不在白名单内则进入下一步,进行token有效验证

    最新版本的token已经改成了JWT模式,JWT对比token最大的好处就是验证时不需要访问数据库

    但是JWT是去中心化的,无法进行废弃(在有效期截止前无法退出登录),所以若依这里还是通过Redis进行了一次登录判断。有兴趣的小伙伴可以了解一下JWT的原理

    验证token是否有效

    在通过token验证之后,将当前登录用户的一些信息添加到Header中,向下个微服务传递

    添加用户信息到Header中

    这样在其他微服务中就可以通过直接获取Header,来获得当前登录用户的信息

  1. 使用注解@RequiresPermissions进行接口鉴权

    首先系统在com.ruoyi.common.security.aspect.PreAuthorizeAspect中定义切点

    定义AOP签名

    在spring.factories中添加自动装配

自动装配

AOP定义完成后,当请求进入带有注解@RequiresPermissions的接口时,就会进行进入切面进行接口鉴权

依次对@RequiresLogin、@RequiresRoles和@RequiresPermissions进行校验

注解鉴权

@RequiresPermissions的校验

当注解@RequiresPermissions中包含多个接口权限时,可以指定验证模式(默认为AND)

验证模式默认为AND

判断鉴权模式

按照一定规则(OR | AND),判断权限列表中是否拥有当前访问接口的权限

以AND模式进行鉴权验证

校验不通过则抛出异常,接口返回鉴权失败的错误信息

校验通过,既接口认证鉴权完成,进入接口方法

数据认证权限

数据认证权限的本质,其实就是在你查询结果的基础上,按照设定的权限等级,对用户ID或部门ID增加一定的过滤条件

实现方式就是在原有的查询SQL基础上,在SQL中的Where条件尾部进行一段SQL注入来实现数据权限的过滤

举个栗子:

这是系统中的一个查询用户列表的接口,接口定义了数据权限

其中deptAlias表示部门表的别名,userAlias表示用户表的别名,这个别名后面时会用到

/**
 * 根据条件分页查询用户列表
 * 
 * @param user 用户信息
 * @return 用户信息集合信息
 */
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
    return userMapper.selectUserList(user);
}

我为查询用户设置数据权限为[ 仅本人数据权限 ]时

分配数据权限

XML代码:

   <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
	select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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="userName != null and userName != ''">
		AND u.user_name like concat('%', #{userName}, '%')
	</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>

加权限前(全部权限)的SQL

select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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'

加权限后的SQL

select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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'
		AND ( {}.user_id = {})

我们可以看到,加权限后的SQL,在尾部多了一段SQL 【 AND ( {}.user_id = {}) 】

其中第一个{}表示在接口注解中定义的userAlias别名,第二个{}表示当前登录用户的ID

转义后的结果就是【 AND ( u.user_id = 1) 】

OK,再让我们把权限设置为[本部门数据权限],进行一下对比

加权限前(全部权限)的SQL

select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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'

加权限后的SQL

select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, 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'
		 AND ( {}.dept_id = {} )

看到这里,我觉得已经不用再解释了,尾部SQL的转义后的结果就是【 AND ( d.dept_id = 226) 】

同理,其他权限范围的实现方式也是一样,在原有的查询SQL基础上,在SQL的where条件尾部进行一段SQL注入来实现数据权限的过滤

原理就是这么简单,接下看我们看一下实现过程

认证过程

首先,我们在使用时通过注解@DataScope来表示接口拥有数据权限

注解中定义了部门表的别名和用户表的别名,用以在执行SQL时指定别名

/**
 * 数据权限过滤注解
 * 
 * @author ruoyi
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
    /**
     * 部门表的别名
     */
    public String deptAlias() default "";

    /**
     * 用户表的别名
     */
    public String userAlias() default "";
}

系统在com.ruoyi.common.datascope.aspect.DataScopeAspect定义了数据认证权限的切面

image-20211105165403722

在spring.factories中添加自动装配

image-20211105165416431

在切面DataScopeAspect中主要完成了三件事

  • 清除params.dataScope参数,防止被SQL注入
  • 判断登录用户所拥有的数据权限,按照权限等级生成对应的SQL
  • 将生成的SQL注入到params.dataScope中

最后,在具体的业务查询SQL中将 ${params.dataScope} 加入到where条件的末尾既可完成注入

5种权限等级对应的注入SQL:

#全部数据权限
 不生成SQL
#自定义数据权限
 OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} )
#部门数据权限
 OR {}.dept_id = {} 
#部门及以下数据权限
 OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )
#仅本人数据权限
 OR {}.user_id = {} 
 

写累了,未完待续~


文章作者: witleo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 witleo !
  目录