相关代码已上传:https://gitee.com/lakernote/easy-admin
已开源基于SpringBoot+Mybatisplus+Layui+SnakerFlow前后端分离轻量级工作流引擎的脚手架项目 easy-admin
为了解决用户和资源的操作关系, 让指定的用户,只能操作指定的资源。
基于资源,英文全程Access Control List
ACL是最早也是最基本的一种访问控制机制,它的原理非常简单:每一项资源,都配有一个权限列表,这个列表记录的就是哪些用户可以对这项资源执行CRUD中的那些操作。
当用户访问某资源时,会先检查这个列表中是否有关于当前用户的访问权限,从而确定当前用户可否执行相应的操作。总得来说,ACL是一种面向资源的访问控制模型,它的机制是围绕“资源”展开的。
优点:
实现简单,方便项目集成。
缺点:
需要维护大量的权限列表,在性能上有明显的缺陷。另外,对于拥有大量用户与众多资源的应用,管理访问控制列表本身就变成非常繁重的工作。
应用场景:在分享资源的场景,某个资源分享给某些人可用,例如分享我们的解说视频给群里的小伙伴。
权限列表示例:
resource_id | user_id | privilege |
---|---|---|
12 | 123 | 读/写/读写等 |
public static final Permission READ = new BasePermission(1 << 0, 'R'); // 1
public static final Permission WRITE = new BasePermission(1 << 1, 'W'); // 2
public static final Permission CREATE = new BasePermission(1 << 2, 'C'); // 4
public static final Permission DELETE = new BasePermission(1 << 3, 'D'); // 8
public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A'); // 16
基于角色,英文全程Role Based Access Control
RBAC是把用户按角色进行归类,通过用户的角色来确定用户能否针对某项资源进行某项操作。
优点:
RBAC相对于ACL最大的优势就是它简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来,而用户与权限变成了间接关联。
RBAC模型使得访问控制,特别是对用户的授权管理变得非常简单和易于维护,因此有广泛的应用。
除两上述两种主要的模型之外,还有包括:基于属性的访问控制ABAC和基于策略的访问控制PBAC等等。
RBAC还有其他几种变种,但是核心一样。
除了数据权限我看目前的实现都是基于RBAC做个权限标识符集合做判断,这里我们只讨论数据权限的。
菜单按钮类
此图来着互联网
菜单、按钮、接口权限一般就是标识符集合。
常规的业务系统,数据粒度主要分为如下几种:
全部数据权限
部门数据权限:查看用户所在部门的数据。
部门及以下数据权限:查看用户所在部门及下属部门的数据。
本人数据权限:只能查看自己的数据。
自定义数据权限
有用这种sql改造型的,也有结合上面ACL模式实现的,具体情况请结合业务选择,这里也是讲下SQL改造型的。
原理是在业务sql实际执行前改造原sql,也即是在原sql查询条件加上create_by = xxx
,create_dept_id = xxx
等过滤条件。
例如原业务sql如下:
select * from leave where leave_day > #{day}
如果当前用户只有本人数据权限则改造后sql如下:
select * from leave where leave_day > #{day} and create_by = #{currentUserId}
使用内置的DataPermissionInterceptor
拦截器。
第一步:实现自己的DataPermissionHandler
/**
* 这种只能处理查询 不能处理 cud
* 且不支持别名
*/
@Slf4j
public class LakerDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
List<String> split = StrUtil.split(mappedStatementId, '.');
...
try {
switch (dataPower.get().getDataFilterType()) {
// 查看全部
case ALL:
return where;
// 查看本人所在组织机构以及下属机构
case DEPT_SETS:
// 创建IN 表达式
// 创建IN范围的元素集合
Set<Long> deptIds = userInfoAndPowers.getDeptIds();
// 把集合转变为JSQLParser需要的元素列表
ItemsList itemsList = new ExpressionList(deptIds.stream().map(LongValue::new).collect(Collectors.toList()));
InExpression inExpression = new InExpression(new Column("create_dept_id"), itemsList);
AndExpression andExpression = new AndExpression(where, inExpression);
log.info(WHERE, andExpression);
return andExpression;
// 查看当前部门的数据
case DEPT:
// = 表达式
// dept_id = deptId
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("create_dept_id"));
equalsTo.setRightExpression(new LongValue(userInfoAndPowers.getDeptId()));
// 创建 AND 表达式 拼接Where 和 = 表达式
// WHERE xxx AND dept_id = 3
AndExpression deptAndExpression = new AndExpression(where, equalsTo);
log.info(WHERE, deptAndExpression);
return deptAndExpression;
// 查看自己的数据
case SELF:
// create_by = userId
EqualsTo selfEqualsTo = new EqualsTo();
selfEqualsTo.setLeftExpression(new Column("create_by"));
selfEqualsTo.setRightExpression(new LongValue(userInfoAndPowers.getUserId()));
AndExpression selfAndExpression = new AndExpression(where, selfEqualsTo);
log.info(WHERE, selfAndExpression);
return selfAndExpression;
case DIY:
return new AndExpression(where, new StringValue(userInfoAndPowers.getSql()));
default:
break;
}
} catch (Exception e) {
log.error("LakerDataPermissionHandler.err", e);
}
...
return where;
}
}
第二步:把数据权限拦截器加入到拦截器链路中
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加数据权限插件
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
LakerDataPermissionHandler lakerDataPermissionHandler = new LakerDataPermissionHandler();
// 添加自定义的数据权限处理器
dataPermissionInterceptor.setDataPermissionHandler(lakerDataPermissionHandler);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
但是这种实现方式,只支持数据查询权限,不支持数据删除权限、数据修改权限等。
第一步:实现自定义的权限拦截器,默认的权限拦截器只有beforeQuery()
public class LakerDataPermissionV2Interceptor extends JsqlParserSupport implements InnerInterceptor {
private LakerV2DataPermissionHandler dataPermissionHandler = new LakerV2DataPermissionHandler();
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) return;
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect, (String) obj);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
parserSingle(mpBs.sql(), ms.getId());
}
...
}
第二步:实现自己的DataPermissionHandler
public class LakerV2DataPermissionHandler {
public static final String WHERE = " where {}";
@SneakyThrows
public Expression getSqlSegment(PlainSelect plainSelect, String mappedStatementId) {
// 获取原SQL Where 条件表达式
Expression where = plainSelect.getWhere();
// 获取sql语句的from 主表
Table fromItem = (Table) plainSelect.getFromItem();
// 有别名用别名,无别名用表名,防止字段冲突报错
Alias fromItemAlias = fromItem.getAlias();
String mainTableName = fromItemAlias == null ? fromItem.getName() : fromItemAlias.getName();
...
switch (dataPower.get().getDataFilterType()) {
// 查看自己的数据
case SELF:
// create_by = userId
EqualsTo selfEqualsTo = new EqualsTo();
selfEqualsTo.setLeftExpression(new Column(mainTableName + ".create_by"));
selfEqualsTo.setRightExpression(new LongValue(userInfoAndPowers.getUserId()));
AndExpression selfAndExpression = new AndExpression(where, selfEqualsTo);
log.info(WHERE, selfAndExpression);
return selfAndExpression;
case DIY:
return new AndExpression(where, new StringValue(userInfoAndPowers.getSql()));
default:
break;
...
}
}
第三步:把数据权限拦截器加入到拦截器链路中
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// V2版本
LakerDataPermissionV2Interceptor dataPermissionInterceptor = new LakerDataPermissionV2Interceptor();
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
原始sql
SELECT * FROM ext_leave WHERE (leave_day >= ?)
数据权限过滤sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) AND ext_leave.create_by = 16
原始sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) LIMIT ?
数据权限过滤sql
SELECT * FROM ext_leave WHERE (leave_day >= ?) AND ext_leave.create_by = 16 LIMIT ?
原始sql
SELECT l.*, u.nick_name uNickName, d.dept_name uDeptName
FROM ext_leave l
LEFT JOIN sys_user u ON u.user_id = l.create_by
LEFT JOIN sys_dept d ON d.dept_id = u.dept_id
WHERE (l.leave_day >= ?) ORDER BY l.create_time DESC LIMIT ?
数据权限过滤sql
SELECT l.*, u.nick_name uNickName, d.dept_name uDeptName
FROM ext_leave l
LEFT JOIN sys_user u ON u.user_id = l.create_by
LEFT JOIN sys_dept d ON d.dept_id = u.dept_id
WHERE (l.leave_day >= ?) AND l.create_by = 16 ORDER BY l.create_time DESC LIMIT ?
别名情况也能自动识别