ruoyi-vue-plus-权限控制
ruoyi-vue-plus应用文档 : https://plus-doc.dromara.org (opens new window)
# 权限控制
该框架主要以 部门 和 角色 两个维度进行权限分配
前置说明
- 表必须包含字段 :
user_id
和dept_id
作为 当前表的数据控制 - 新增数据前必须为以上两个字段进行添加值
涉及基本库表
表 | 说明 |
---|---|
sys_dept | 部门表 |
sys_role | 角色表 |
sys_user | 用户表 |
sys_role_dept | 角色部门关联表 |
sys_user_role | 用户角色关联表 |
sys_role_menu | 角色菜单关联表 |
# 部门管理
顾名思义 , 就是将用户划分不同组织 , 方便管理
不涉及整体权限控制 , 仅用 dept_id 划分范围应用
# 角色管理
角色在系统中 , 充当相对重要的功能 , 能够划分 功能细分化 , 根据不同用户划分角色即可分配权限等明细应用!
# 菜单权限
菜单权限的赋予 , 在库表中 , 也不难发现 , sys_role_menu
关联表 主要实现 不同角色赋予不同的菜单
# 数据权限
数据权限的控制是比角色更为细化的权限分配 , 是在库表中的字段层面控制 , 主要通过 user_id
和 dept_id
字段所控制
权限分配分为几个角度进行约束 , 以及 SQL约束结构 . 自定义拓展约束点击跳转
全部数据权限 (无约束)
自定义数据权限 (指定部门约束)
dept_id IN ( xx , xx , .. )
本部门约束 (当前部门约束)
dept_id = xx
本部门及以下数据权限 (父子联动)
dept_id IN ( xx, xx, .. )
仅本人数据权限
user_id = xx
提示
在实际场景中 , 可通过多身份实现SQL约束 . 也就是说给即将要限制的 用户 加多一个 本部门及以下数据权限 即可
# 实现原理
通过 拦截器InnerInterceptor接口 , 重写 sql 操作的方法 . 最为核心的还是使用了 SpEl表达式解析 (opens new window)
约束过程必要信息
- 权限范围 根据不同身份划分 , 从而锁定
DataScopeType
枚举 中指定的模板 - SQL填充的字段名 , 根据 Mapper层 的
@DataPermission&@DataColumn
注解 - SQL填充的匹配值
- 根据
DataPermissionHelper
类 请求上下文临时存储使用 (使用方式跟Map误差) - 根据 Bean解析器 , 调取方法返回 字符串 填充
- 根据
提示
@DataPermission
可以写在类上 , 使其类的所有方法均可生效
涉及类
类 | 说明 |
---|---|
DataScopeType | 数据权限模板定义 |
DataPermission | 数据权限组注解 |
DataColumn | 具体的数据全新字段标注 |
PlusDataPermissionInterceptor | 数据权限SQL拦截器 |
PlusDataPermissionHandler | 数据权限处理器 |
DataPermissionHelper | 数据权限助手 |
SysDataScopeService | 自定义Bean处理数据权限 |
提示
以上关键类 , 可以进行点击跳转
# DataScopeType数据权限模板定义
点击展开
@Getter
@AllArgsConstructor
public enum DataScopeType {
/**
* 全部数据权限
*/
ALL("1", "", ""),
/**
* 自定数据权限
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),
/**
* 部门数据权限
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),
/**
* 部门及以下数据权限
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),
/**
* 仅本人数据权限
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
/**
* 模板唯一
*/
private final String code;
/**
* 语法 采用 spel 模板表达式
*/
private final String sqlTemplate;
/**
* 不满足 sqlTemplate 则填充
*/
private final String elseSql;
public static DataScopeType findCode(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
for (DataScopeType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
}
可以看到拼接的占位符应用
占位符 | 说明 |
---|---|
#{#deptName} | 在 @DataColumn 注解 中的key 对应的模板占位符填充其值 |
#{#user.userId} | 在 LoginHelper 类 中拿去当前用户userId |
#{@sdss.getRoleCustom( #user.roleId )} | 通过在 SysDataScopeService 自定义的Bean中调取方法拿到字符串 |
#{@sdss.getDeptAndChild( #user.deptId )} ) | 通过在 SysDataScopeService 自定义的Bean中调取方法拿到字符串 |
# DataPermission&DataColumn数据权限组注解
嵌套复合注解应用
点击展开
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
DataColumn[] value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {
/**
* 占位符关键字
*/
String[] key() default "deptName";
/**
* 占位符替换值
*/
String[] value() default "dept_id";
}
# PlusDataPermissionInterceptor数据权限SQL拦截器
数据权限SQL拦截器 . 继承了 JsqlParserSupport
(sql解析器) , 实现了 InnerInterceptor
(MP内部拦截器)
拦截重写的方法意图
- beforeQuery() 查询前 : 检查 忽略注解 和 无注解方法 , 并且通过 JSqlParser工具 将SQL解析 , 方便操作
- beforePrepare() 准备前 : 对 删除和修改 通过 JSqlParser工具 将SQL解析 , 方便操作
- processSelect() 查询操作 : 对 查询 进行SQL约束处理
- processUpdate() 修改操作 : 对 修改 进行SQL约束处理
- processDelete() 删除操作 : 对 删除 进行SQL约束处理
对SQL约束处理的核心方法 dataPermissionHandler.getSqlSegment()
点击展开
public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 检查忽略注解
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
// 检查是否无效 无数据权限注解
if (dataPermissionHandler.isInvalid(ms.getId())) {
return;
}
// 解析 sql 分配对应方法
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
// 准备前处理更新 和 删除
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
}
}
/**
* 查询前 , 重写sql
*/
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
// 简单查询
this.setWhere((PlainSelect) selectBody, (String) obj);
} else if (selectBody instanceof SetOperationList) {
// 联合查询
SetOperationList setOperationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
/**
* 修改前 , 重写sql
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
if (null != sqlSegment) {
update.setWhere(sqlSegment);
}
}
/**
* 删除前 , 重写sql
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
if (null != sqlSegment) {
delete.setWhere(sqlSegment);
}
}
/**
* 设置 where 条件
*
* @param plainSelect 查询对象
* @param mappedStatementId 执行方法id
*/
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
}
# PlusDataPermissionHandler数据权限处理器
数据权限过滤 核心处理类
大致流程
获取注解填充数据 , 根据全限定名方法 反射获取
DataColumn[] dataColumns = findAnnotation(mappedStatementId);
请求上下文设置参数填充 值 (当前用户)
DataPermissionHelper.setVariable("user", currentUser);
构建SQL Where约束
String dataFilterSql = buildDataFilter(dataColumns, isSelect);
响应 SQL约束表达式 设置where约束
plainSelect.setWhere(sqlSegment);
点击展开
@Slf4j
public class PlusDataPermissionHandler {
/**
* 方法或类(名称) 与 注解的映射关系缓存
*/
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
/**
* 无效注解方法缓存用于快速返回
*/
private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();
/**
* spel 解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
private final ParserContext parserContext = new TemplateParserContext();
/**
* bean解析器 用于处理 spel 表达式中对 bean 的调用
*/
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
/**
* SQL 拼接核心方法
* @param where 约束操作的表达式
* @param mappedStatementId mapper全限定名路径
* @param isSelect 是否查询
*/
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
DataColumn[] dataColumns = findAnnotation(mappedStatementId);
// 缓存无效注解 , 避免不必要的数据加载
if (ArrayUtil.isEmpty(dataColumns)) {
// 将没有注解的方法缓存起来 , 根据全限定名方法判断是否含有缓存
invalidCacheSet.add(mappedStatementId);
return where;
}
// 存储当前用户 user , 一遍请求应用
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员,则不过滤数据
if (LoginHelper.isAdmin()) {
return where;
}
// 拿到注解信息构建 约束sql
String dataFilterSql = buildDataFilter(dataColumns, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
try {
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限使用单独的括号 防止与其他条件冲突
Parenthesis parenthesis = new Parenthesis(expression);
if (ObjectUtil.isNotNull(where)) {
return new AndExpression(where, parenthesis);
} else {
return parenthesis;
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
}
}
/**
* 构造数据过滤sql
*/
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
// 更新或删除需满足所有条件 (查询采用 OR ; 删除/修改 采用 AND)
String joinStr = isSelect ? " OR " : " AND ";
LoginUser user = DataPermissionHelper.getVariable("user");
// 评估对象 (用户构建约束sql)
StandardEvaluationContext context = new StandardEvaluationContext();
// 设置 bean解析器
context.setBeanResolver(beanResolver);
// 将 请求的所有上下文 设置到 context 中
DataPermissionHelper.getContext().forEach(context::setVariable);
Set<String> conditions = new HashSet<>();
for (RoleDTO role : user.getRoles()) {
user.setRoleId(role.getRoleId());
// 获取角色权限泛型 (根据其对象获取模板sql)
DataScopeType type = DataScopeType.findCode(role.getDataScope());
if (ObjectUtil.isNull(type)) {
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
}
// 全部数据权限直接返回
if (type == DataScopeType.ALL) {
return "";
}
boolean isSuccess = false;
// 拼接环节
for (DataColumn dataColumn : dataColumns) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
// 不包含 key 变量 则不处理
if (!StringUtils.containsAny(type.getSqlTemplate(),
Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
)) {
continue;
}
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}
// 解析sql模板并填充 (bean解析填充)
// bean解析器调用方法填充数据
String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
conditions.add(joinStr + sql);
isSuccess = true;
}
// 未处理成功则填充替补模板方案
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
conditions.add(joinStr + type.getElseSql());
}
}
if (CollUtil.isNotEmpty(conditions)) {
// 将 conditions 内的 字符串按顺序拼接起来
String sql = StreamUtils.join(conditions, Function.identity(), "");
/**
* 拼接后的结果可能为以下结果 , 需要排除前缀
* OR dept_id IN ( xx,xx,xx ) OR ...
*/
return sql.substring(joinStr.length());
}
return "";
}
/**
* 通过方法的全限定名称进行反射解析获取反射数据
*/
private DataColumn[] findAnnotation(String mappedStatementId) {
StringBuilder sb = new StringBuilder(mappedStatementId);
int index = sb.lastIndexOf(".");
// 类全限定名路径
String clazzName = sb.substring(0, index);
// 方法名
String methodName = sb.substring(index + 1, sb.length());
Class<?> clazz = ClassUtil.loadClass(clazzName);
// 过滤掉不相符的方法名
List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
.filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
DataPermission dataPermission;
// 获取方法注解
for (Method method : methods) {
dataPermission = dataPermissionCacheMap.get(mappedStatementId);
if (ObjectUtil.isNotNull(dataPermission)) {
return dataPermission.value();
}
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
return dataPermission.value();
}
}
// 检查类是否包含 DataPermission注解 并且获取
dataPermission = dataPermissionCacheMap.get(clazz.getName());
if (ObjectUtil.isNotNull(dataPermission)) {
return dataPermission.value();
}
// 获取类注解
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
return dataPermission.value();
}
return null;
}
/**
* 是否为无效方法 无数据权限
*/
public boolean isInvalid(String mappedStatementId) {
return invalidCacheSet.contains(mappedStatementId);
}
}
# DataPermissionHelper数据权限助手
数据权限助手 . 通过 SaHolder.getStorage() , 拿到请求上下文存储数据
点击展开
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked cast")
public class DataPermissionHelper {
private static final String DATA_PERMISSION_KEY = "data:permission";
public static <T> T getVariable(String key) {
Map<String, Object> context = getContext();
return (T) context.get(key);
}
public static void setVariable(String key, Object value) {
Map<String, Object> context = getContext();
context.put(key, value);
}
public static Map<String, Object> getContext() {
SaStorage saStorage = SaHolder.getStorage();
Object attribute = saStorage.get(DATA_PERMISSION_KEY);
if (ObjectUtil.isNull(attribute)) {
saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
attribute = saStorage.get(DATA_PERMISSION_KEY);
}
if (attribute instanceof Map) {
return (Map<String, Object>) attribute;
}
throw new NullPointerException("data permission context type exception");
}
/**
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
}
/**
* 关闭忽略数据权限
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
/**
* 在忽略数据权限中执行
*
* @param handle 处理执行方法
*/
public static void ignore(Runnable handle) {
enableIgnore();
try {
handle.run();
} finally {
disableIgnore();
}
}
/**
* 在忽略数据权限中执行
*
* @param handle 处理执行方法
*/
public static <T> T ignore(Supplier<T> handle) {
enableIgnore();
try {
return handle.get();
} finally {
disableIgnore();
}
}
}
# SysDataScopeServiceBean处理权限
数据权限 服务 , 通过 Bean解析器 解析调用方法 , 对其SQL进行填充约束值
如果自定义写一个Bean , 并且是用于模板中填充使用的方法 , 则当前Service不能 调用含有
@DataPermission
注解 方法 , 可能会导致递归死循环问题
点击展开
@RequiredArgsConstructor
@Service("sdss")
public class SysDataScopeServiceImpl implements ISysDataScopeService {
private final SysRoleDeptMapper roleDeptMapper;
private final SysDeptMapper deptMapper;
@Override
public String getRoleCustom(Long roleId) {
List<SysRoleDept> list = roleDeptMapper.selectList(
new LambdaQueryWrapper<SysRoleDept>()
.select(SysRoleDept::getDeptId)
.eq(SysRoleDept::getRoleId, roleId));
if (CollUtil.isNotEmpty(list)) {
return StreamUtils.join(list, rd -> Convert.toStr(rd.getDeptId()));
}
return null;
}
@Override
public String getDeptAndChild(Long deptId) {
List<SysDept> deptList = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
.select(SysDept::getDeptId)
.apply(DataBaseHelper.findInSet(deptId, "ancestors")));
List<Long> ids = StreamUtils.toList(deptList, SysDept::getDeptId);
ids.add(deptId);
List<SysDept> list = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
.select(SysDept::getDeptId)
.in(SysDept::getDeptId, ids));
if (CollUtil.isNotEmpty(list)) {
return StreamUtils.join(list, d -> Convert.toStr(d.getDeptId()));
}
return null;
}
}
# 自定义数据权限
大概流程
- 在前端数据权限的下拉框中追加标识 src/views/system/role/index.vue dataScopeOptions数组追加对象 (写死)
- 在
DataScopeType
类 中 , 写一个自己的sql约束模板 , 需要需要对应上 - 在 mapper层 , 使用
@DataPermission
注解 填充占位符 - 在调用 mapper方法 前 , 可通过 *DataPermissionHelper.serVariable()*方法 , 将其值填充到模板
- 测试查询观察SQL拼接情况 ...
注意
- 在写SQL模板时 , 前后部分建议追加一个空格 , 防止拼接时 前后混淆
DataPermissionHelper
类 中设置的值 , 仅在本请求上下文有效