结合(Spring Security + MyBatis-Plus)以及数据中台的通用架构,梳理了一套完整的权限设计方案,包含架构分层、核心设计以及时序交互流程。
🏗️ 一、 整体架构设计
在数据中台中,权限体系通常分为三个维度,你提到的这三者各司其职:
- 功能权限 (Spring Security):控制"你能看什么页面、点什么按钮"。基于 RBAC(基于角色的访问控制)模型,通过菜单和按钮权限控制前端界面的可见性。
- 项目权限 (MyBatis-Plus) :控制"你能进哪个项目"。数据中台通常涉及多项目隔离,通过拦截 SQL,在查询项目相关数据时自动注入
project_id = X的过滤条件。 - 数据权限 (MyBatis-Plus):控制"你能看项目里的哪些数据行/列"。即行级权限(如:仅看本部门数据)和列级权限(如:薪资字段对普通员工不可见)。
⚙️ 二、 核心实现方案
1. 功能权限:基于 Spring Security
- 实现方式 :使用 Spring Security 的
@PreAuthorize注解配合 SpEL(Spring Expression Language)。 - 原理 :
- 用户登录时,
UserDetailsService从数据库加载用户的角色和权限列表(如project:admin,data:query)。 - 在 Controller 或 Service 方法上使用注解,例如
@PreAuthorize("hasAuthority('DATA_QUERY')")。 - 对于项目级别的入口控制,可以结合路径变量,例如
@PreAuthorize("#projectId == authentication.projectId")来校验用户是否有权访问该特定项目。
- 用户登录时,
2. 项目权限 & 数据权限:基于 MyBatis-Plus 拦截器
- 实现方式 :利用 MyBatis-Plus 的
DataPermissionInterceptor或自定义InnerInterceptor。 - 原理 :
- 拦截 SQL :在 SQL 执行前(
beforeQuery),拦截所有的SELECT语句。 - 解析注解 :检查 Mapper 或 Service 方法上是否有自定义的权限注解(如
@DataScope)。 - 动态拼接 :
- 项目权限 :根据当前登录用户上下文中的
currentProjectId,自动拼接AND project_id = ?。 - 数据权限 :根据用户的角色(如部门经理、普通员工),拼接不同的 WHERE 条件,例如
AND dept_id IN (1,2)。
- 项目权限 :根据当前登录用户上下文中的
- 拦截 SQL :在 SQL 执行前(
⏱️ 三、 请求完整的时序交互
这是一个用户发起数据查询请求(例如:查询某项目下的销售报表)的完整时序图解:
👤 1. 认证与功能鉴权阶段
- 用户请求 :用户携带 Token(如 JWT)访问数据中台的查询接口
/api/report/sales?projectId=100。 - JWT 过滤器 :
JwtAuthenticationTokenFilter拦截请求,解析 Token,将用户信息(包含用户ID、角色列表、权限字符串)存入SecurityContextHolder。 - Spring Security 鉴权 :
- 框架检查该接口所需的权限(例如
REPORT_VIEW)。 - 对比当前用户拥有的权限。
- 结果:如果用户没有功能权限,直接返回 403 Forbidden,请求结束;如果有权限,进入业务逻辑层。
- 框架检查该接口所需的权限(例如
🔍 2. 项目与数据权限处理阶段
- 业务逻辑处理:Controller 调用 Service 层方法。
- 注解识别 :假设 Service 方法上标注了
@DataScope(deptAlias = "d", projectAlias = "p")。 - MyBatis-Plus 拦截 :
DataPermissionInterceptor拦截到即将执行的 SQL 查询。- 获取上下文:从 ThreadLocal 或 SecurityContext 中获取当前用户对象。
- 生成过滤片段 :
- 项目权限 :检查用户是否属于项目 100,生成
p.id = 100。 - 数据权限 :检查用户角色(如"华东区经理"),生成
d.region = 'EastChina'。
- 项目权限 :检查用户是否属于项目 100,生成
- SQL 改写 :拦截器将原始 SQL:
SELECT * FROM sales s JOIN dept d ON s.dept_id = d.id
动态改写为:
SELECT * FROM sales s JOIN dept d ON s.dept_id = d.id WHERE p.id = 100 AND d.region = 'EastChina'。
📊 3. 数据执行与返回
- 数据库执行:改写后的 SQL 发送到数据库执行。
- 结果返回:数据库返回过滤后的数据集给用户。
📊 四、 权限类型与技术实现对照表
| 权限类型 | 控制粒度 | 核心技术 | 实现机制 | 典型场景 |
|---|---|---|---|---|
| 功能权限 | 菜单/按钮/API | Spring Security | @PreAuthorize 注解 + 角色权限匹配 |
普通用户看不到"系统管理"菜单 |
| 项目权限 | 项目/工作空间 | MyBatis-Plus 拦截器 | SQL 拦截 + project_id 自动注入 |
用户 A 只能看到项目 A 的数据,看不到项目 B |
| 数据权限 | 数据行/列 | MyBatis-Plus 拦截器 | SQL 拦截 + 动态 WHERE 条件拼接 | 销售员只能看自己的订单,经理看全组订单 |
💡 五、 关键代码逻辑示意
1. MyBatis-Plus 拦截器核心逻辑
// 实现 InnerInterceptor 接口
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 1. 获取当前用户信息
LoginUser user = SecurityUtils.getLoginUser();
// 2. 判断是否需要数据权限(排除管理员)
if (user.isAdmin()) return;
// 3. 获取注解配置的表别名
String deptAlias = getDataScopeAlias(ms, "dept");
String projectAlias = getDataScopeAlias(ms, "project");
// 4. 拼接 SQL 片段
StringBuilder sqlFilter = new StringBuilder();
// 项目权限:限制只能看自己加入的项目
sqlFilter.append(projectAlias).append(".id IN (").append(user.getProjectIds()).append(") ");
// 数据权限:根据角色限制数据范围 (例如:仅本人、本部门)
if ("dept".equals(user.getRole().getDataScopeType())) {
sqlFilter.append(" OR ").append(deptAlias).append(".id = ").append(user.getDeptId());
}
// 5. 将拼接好的 SQL 条件注入到查询参数中
// MyBatis-Plus 会自动将这个条件合并到原生 SQL 的 WHERE 后面
injectFilterToParameter(parameter, sqlFilter.toString());
}
2. Service 层使用注解
@Service
public class ReportService {
// dataScope 注解指定了部门表别名和项目表别名
@DataScope(deptAlias = "d", projectAlias = "p")
public List<SalesReport> getReports(Long projectId) {
// 这里写正常的业务查询,无需手动写 project_id 和 dept_id 的过滤
// 拦截器会自动帮你加上
return salesMapper.selectReports(projectId);
}
}
📌 六、 总结建议
这套方案的优势在于解耦 和透明:
- 开发友好 :业务开发人员在写 Mapper 时,只需要关注业务逻辑 SQL,不需要在每个 SQL 里都手写
WHERE project_id = ?,由拦截器统一处理。 - 安全统一:Spring Security 负责大门(功能)的钥匙,MyBatis-Plus 负责数据库(数据)的过滤网,两者结合能有效防止越权访问。
在实际落地时,先搭建好用户-角色-权限的管理后台,确保管理员可以灵活配置某个角色是"本部门数据"还是"全部数据",并将这些配置存储在数据库中,供拦截器读取。