在数据分析领域,权限管理一直是让人头疼的问题。传统的开源 RBAC 框架,虽然能够提供一定的权限控制,但在面对复杂的数据权限需求时,往往需要修改代码来实现。例如,一个需求是只允许特定部门的员工查看包含部门标识的订单信息,这种情况下,我们就需要在代码中添加各种 if-else 判断,权限逻辑与业务逻辑耦合在一起,维护起来非常痛苦。如何才能告别改权限就要改代码的魔咒,实现真正的配置即权限?本文将以 SPARK 为例,介绍一种六层数据护盾的设计方案。
传统 RBAC 的局限性
传统的 RBAC (Role-Based Access Control) 框架,通常包括用户 (User)、角色 (Role)、权限 (Permission) 三个核心概念。用户通过被赋予角色,角色再关联相应的权限,从而实现对资源的访问控制。
这种模型在简单的场景下可以满足需求,但在以下几个方面存在局限性:
- 权限粒度不够细:RBAC 通常只能控制用户对整个表或整个字段的访问权限,无法控制到行级别或更细粒度的权限,例如前面提到的只允许特定部门的员工查看包含部门标识的订单信息。
- 权限变更需要修改代码:当权限需求发生变化时,例如需要新增一种角色或修改某个角色的权限,通常需要修改代码来实现,增加了维护成本和风险。
- 缺乏动态权限控制:RBAC 无法根据用户的属性或上下文信息来动态调整权限,例如根据用户所在地域来限制对某些数据的访问。
开源 RBAC 框架的常见问题
以常用的开源 RBAC 框架为例,例如 Apache Shiro 或 Spring Security,虽然提供了灵活的权限管理功能,但在数据权限控制方面仍然存在一些问题。例如,我们需要在代码中使用注解或配置文件来定义权限规则,当权限规则发生变化时,就需要修改代码并重新部署应用程序。在微服务架构中,如果每个服务都需要维护一套独立的权限管理系统,那么维护成本将会非常高。
SPARK 六层数据护盾设计
为了解决传统 RBAC 的局限性,我们设计了一种基于配置即权限的六层数据护盾方案,该方案的核心思想是将权限控制逻辑从代码中解耦出来,通过配置文件来实现权限的动态管理。这六层分别是:
- 认证层 (Authentication):负责验证用户的身份,例如用户名密码、OAuth 2.0 等。
- 接入层 (Access Control):负责控制用户对资源的访问,例如 HTTP 接口、数据库连接等。可以使用 Nginx 反向代理,并通过 Lua 脚本进行简单的权限验证。同时需要考虑高并发场景下的性能问题,例如调整 Nginx 的 worker 进程数和连接超时时间。
- 数据层 (Data Filtering):负责对数据进行过滤,例如行级别过滤、列级别过滤等。这是最核心的一层,需要根据用户的角色和属性来动态生成 SQL 查询条件。
- 脱敏层 (Data Masking):负责对敏感数据进行脱敏处理,例如身份证号、手机号等。可以使用不同的脱敏算法,例如替换、加密、截断等。
- 审计层 (Audit):负责记录用户的访问行为,例如访问时间、访问IP、访问数据等。可以用于安全审计和问题排查。
- 告警层 (Alerting):负责监控用户的异常行为,例如频繁访问敏感数据、尝试绕过权限控制等。可以使用不同的告警策略,例如短信告警、邮件告警等。
数据层(Data Filtering)的实现
数据层是整个数据护盾的核心,负责根据用户的角色和属性来动态生成 SQL 查询条件。具体实现方式如下:
- 定义权限规则:使用 YAML 或 JSON 等格式的配置文件来定义权限规则。每个规则包括角色、属性、条件等信息。例如:
role: sales
attributes:
department: sales
conditions:
order.department_id = ${attributes.department}
解析权限规则:在应用程序中解析权限规则,并将其转换为 SQL 查询条件。可以使用模板引擎,例如 FreeMarker 或 Velocity,来动态生成 SQL 查询条件。
应用权限规则:在执行 SQL 查询之前,将生成的 SQL 查询条件添加到 SQL 语句中。可以使用 SQL 拦截器或代理来实现。
// 使用 MyBatis Plus 拦截器实现数据权限控制
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 SQL 参数
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
// 获取原始 SQL
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 根据用户角色和属性,生成 SQL 查询条件
String dataPermissionSql = generateDataPermissionSql();
// 将 SQL 查询条件添加到 SQL 语句中
String newSql = originalSql + " AND " + dataPermissionSql;
// 重新构建 BoundSql
BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql, boundSql.getParameterMappings(), parameter);
// 重新构建 MappedStatement
MappedStatement newMappedStatement = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
// 重新执行查询
return invocation.proceed(new Object[]{newMappedStatement, parameter, rowBounds, resultHandler});
}
private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(),ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.keyProperty(ms.getKeyProperties());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
// 根据用户角色和属性,生成 SQL 查询条件
private String generateDataPermissionSql() {
// TODO: 从 Session 中获取用户信息
String role = "sales";
String department = "sales";
// TODO: 从配置文件中加载权限规则
String sql = "order.department_id = '" + department + "'";
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
static class BoundSqlSqlSource implements SqlSource {
BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
实战避坑经验
- 权限规则的设计要合理:权限规则的设计要考虑到业务的复杂性和可扩展性,避免出现过于复杂的规则,导致维护困难。
- 权限规则的测试要充分:在上线之前,要对权限规则进行充分的测试,确保权限控制的正确性。
- 性能优化要重视:在动态生成 SQL 查询条件时,要考虑到性能问题,避免出现 SQL 注入等安全漏洞。
- 审计日志要完善:完善的审计日志可以帮助我们及时发现和解决权限问题。
总结
通过配置即权限的六层数据护盾方案,我们可以将权限控制逻辑从代码中解耦出来,实现权限的动态管理,从而告别改权限就要改代码的魔咒。这种方案不仅可以提高开发效率,还可以降低维护成本和风险。在实际应用中,需要根据具体的业务场景和技术架构进行调整和优化,才能达到最佳的效果。
冠军资讯
代码一只喵