简介
由于若依的权限认证是自研开发的,所以总有小伙伴表示若依的权限认证实现过程看不懂,不清楚认证流程,再加上上周权限部分又进行了一次更新。所以我这里基于新版的权限认证详细的带大家学习一下实现过程
由于项目还在不断的更新升级过程中,对相应代码可能存在再次更新调整的可能,具体代码请根据现有版本代码自行判断
本文章基于RuoYi-Cloud v3.2.0版本
前言
其实有很多人在问,框架中为什么没有使用Spring Security OAuth 2.0框架?
这个问题其实是一个无奈的历史问题,其实我们在Git提交记录上可以看到,早期的若依版本其实使用了Spring Security OAuth 2.0框架的,但是在一次更新时移除了OAuth 2.0

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

事件发展过程+双方陈述: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

然后我们通过代码的注释可以看出,@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
我们来进行一次完整的请求访问过程模拟
在系统管理中为用户赋予角色,为角色赋予权限
在Controller中,通过@RequiresPermissions为接口方法声明所属权限

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

通过Redis中的数据可以看出,我刚刚为一名用户赋予了leader的角色,角色中包含用户管理的新增、删除、修改和查询等接口权限
认证过程:
Gateway网关登录鉴权
前端发送的http请求进入Gateway网关模块,首先进入黑名单、验证码和缓存请求等几个过滤器进行一些数据处理,随后进入com.ruoyi.gateway.filter.AuthFilter全局过滤器,进行登录鉴权
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。目前网关统一鉴权
AuthFilter.java就是采用的全局过滤器。单独定义只需要实现
GlobalFilter,Ordered这两个接口就可以了。AuthFilter登录鉴权这里主要完成三件事
判断请求目标是否在白名单内
验证token是否有效
将一部分用户信息添加到Header中

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

通过匹配(在白名单内)则通过登录鉴权,接口无需登录既可访问,不在白名单内则进入下一步,进行token有效验证
最新版本的token已经改成了JWT模式,JWT对比token最大的好处就是验证时不需要访问数据库
但是JWT是去中心化的,无法进行废弃(在有效期截止前无法退出登录),所以若依这里还是通过Redis进行了一次登录判断。有兴趣的小伙伴可以了解一下JWT的原理

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

这样在其他微服务中就可以通过直接获取Header,来获得当前登录用户的信息
使用注解@RequiresPermissions进行接口鉴权
首先系统在com.ruoyi.common.security.aspect.PreAuthorizeAspect中定义切点

在spring.factories中添加自动装配

AOP定义完成后,当请求进入带有注解@RequiresPermissions的接口时,就会进行进入切面进行接口鉴权
依次对@RequiresLogin、@RequiresRoles和@RequiresPermissions进行校验


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


按照一定规则(OR | 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定义了数据认证权限的切面

在spring.factories中添加自动装配

在切面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 = {}
写累了,未完待续~