新增内容: 1. 添加 AGENTS.md 和 CLAUDE.md AI 助手配置文件 2. 添加安全修复说明文档 (doc/security-fixes.md) 3. 新增单元测试用例: - fund-common: TenantContextHolderTest, UserContextHolderTest, PageResultTest, ResultTest - fund-sys: AuthServiceImplTest, RoleServiceImplTest, TenantServiceImplTest 修改内容: 1. 数据库初始化脚本更新 (fund_sys_init.sql) 2. 前端依赖更新 (package.json) 3. 登录和密码管理功能优化: - 管理后台和移动端登录页面 - 密码修改功能 4. 租户上下文处理优化 (TenantLineHandlerImpl) 5. 网关过滤器增强: - TenantGatewayFilter 租户过滤 - TokenAuthFilter 认证过滤 6. Controller 层代码优化 7. DTO 和 Service 层代码改进 技术改进: - 密码加密方式从 BCrypt 改为 MD5(前后端一致) - 登录验证流程优化,支持多租户 - 增加日志输出便于调试 - 代码规范性和可维护性提升
6.9 KiB
租户隔离安全漏洞修复记录
修复日期: 2026-03-01 修复分支: master 修复范围: 多租户数据隔离机制
漏洞一:网关未覆盖客户端伪造的 X-Tenant-Id(高危)
漏洞描述
TokenAuthFilter 在 Token 验证通过后,使用 Spring WebFlux 的 request.mutate().header() 将 Token 中的 tenantId 写入请求头。然而 WebFlux 的 header() 方法是追加而非替换语义,若客户端预先在请求中设置了 X-Tenant-Id,下游服务从 getFirst() 取到的仍然是客户端伪造的值。攻击者可以通过在请求中携带任意 X-Tenant-Id 来访问其他租户的数据。
攻击场景
攻击者发送请求:
Authorization: Bearer <合法Token,属于租户A>
X-Tenant-Id: 999 ← 伪造的租户ID
原始代码中,下游 ContextInterceptor 从请求头取到的 X-Tenant-Id 可能是 999,导致 TenantContextHolder 被设置为 999,进而 MyBatis-Plus 租户插件以 tenant_id=999 过滤 SQL,越权访问其他租户数据。
修复方案
在 TokenAuthFilter 中先移除客户端传来的 X-Tenant-Id,再写入 Token 中已认证的值:
// 安全修复:先移除客户端传来的 X-Tenant-Id,再写入 Token 中已认证的值
ServerHttpRequest mutatedRequest = request.mutate()
.headers(headers -> headers.remove(TENANT_ID_HEADER)) // 先删除客户端值
.header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId()))
.header(USERNAME_HEADER, tokenInfo.getUsername())
.header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId())) // 再写入认证值
.build();
修复位置
fund-gateway/src/main/java/com/fundplatform/gateway/filter/TokenAuthFilter.java 第 86-93 行
漏洞二:IGNORE_TABLES 包含业务敏感表(高危)
漏洞描述
TenantLineHandlerImpl.IGNORE_TABLES 中包含了 sys_user、sys_role、sys_dept、sys_config 等业务表,这些表均有 tenant_id 字段,属于租户私有数据。将其加入忽略列表后,MyBatis-Plus 不会为这些表的查询自动注入 tenant_id 过滤条件,导致以下问题:
- 跨租户用户数据泄漏:查询
sys_user时返回所有租户的用户数据 - 跨租户角色泄漏:查询
sys_role时返回所有租户的角色配置 - 跨租户部门泄漏:查询
sys_dept时返回所有租户的部门结构
修复方案
IGNORE_TABLES 中仅保留真正全平台共享的静态数据表:
// 修复后:仅保留平台级全局共享表
private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList(
"sys_menu", // 菜单表(系统菜单结构全局共享)
"sys_dict", // 字典表(枚举数据全局共享)
"sys_log", // 日志表(独立存储逻辑)
"gen_table", // 代码生成表
"gen_table_column"
));
// 已移除:sys_user、sys_role、sys_dept、sys_config(均为租户私有数据)
修复位置
fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java 第 34-46 行
漏洞三:租户 ID 缺失时 Fallback 到默认值 1(中危)
漏洞描述
TenantLineHandlerImpl.getTenantId() 中,当从 TenantContextHolder 获取不到租户 ID 时,代码回退到默认值 1L:
// 漏洞代码
if (tenantId == null) {
tenantId = 1L; // 危险:误操作租户1的数据
}
这导致在没有认证上下文的场景(如定时任务、内部错误等)下,SQL 会意外查询租户1的数据,可能造成租户1数据泄漏或误写入。
修复方案
当租户上下文缺失时直接抛出异常,中断 SQL 执行:
// 修复后:缺失租户上下文时强制报错
if (tenantId == null) {
throw new IllegalStateException("[Security] 当前请求缺少租户上下文,拒绝执行SQL");
}
修复位置
fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java 第 56-66 行
漏洞四:登录接口跨租户用户名混乱(中危)
漏洞描述
登录接口 /auth/login 在 Token 白名单中,TenantContextHolder 为空。由于 sys_user 原来在 IGNORE_TABLES 中(全局查询),登录时仅按用户名查询不区分租户:
// 漏洞代码:不同租户可能有同名用户,查到哪个是不确定的
wrapper.eq(SysUser::getUsername, request.getUsername());
// 未加 tenantId 条件
SysUser user = userDataService.getOne(wrapper);
当不同租户有同名用户时,认证结果不确定,存在以下风险:
- 用户 A(租户1)可能以租户2用户的身份登录
- 登录成功后 Token 中绑定了错误的 tenantId
修复方案
LoginRequestDTO增加tenantId字段(必填),前端登录时显式指定租户- 登录查询使用
TenantIgnoreHelper跳过自动租户过滤,同时显式加入tenant_id条件:
// 修复后:使用 TenantIgnoreHelper + 显式 tenantId 条件
SysUser user = TenantIgnoreHelper.ignore(() -> {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, request.getUsername());
wrapper.eq(SysUser::getTenantId, request.getTenantId()); // 显式限定租户
wrapper.eq(SysUser::getDeleted, 0);
return userDataService.getOne(wrapper);
});
修复位置
fund-sys/src/main/java/com/fundplatform/sys/dto/LoginRequestDTO.java— 新增 tenantId 字段fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java第 31-44 行
修复总结
| 编号 | 漏洞类型 | 危险级别 | 修复文件 |
|---|---|---|---|
| 1 | 网关未覆盖客户端伪造的 X-Tenant-Id | 高危 | fund-gateway/.../TokenAuthFilter.java |
| 2 | IGNORE_TABLES 包含业务敏感表 | 高危 | fund-common/.../TenantLineHandlerImpl.java |
| 3 | 租户 ID 缺失时 fallback 到默认值 | 中危 | fund-common/.../TenantLineHandlerImpl.java |
| 4 | 登录查询未限定租户范围 | 中危 | fund-sys/.../AuthServiceImpl.java, LoginRequestDTO.java |
修复后的安全保证
- 网关层:经过 Token 验证的请求,
X-Tenant-Id由 Token 中的认证值强制覆盖,客户端无法伪造 - 数据层:业务表(sys_user、sys_role、sys_dept、sys_config)均受 MyBatis-Plus 租户插件保护,自动注入
tenant_id过滤 - 兜底保护:租户上下文为空时 SQL 拒绝执行,不会 fallback 到任何租户
- 登录安全:登录时必须指定 tenantId,确保同名用户不会跨租户混乱认证
注意事项
- 所有需要跨租户操作的合法场景(如超管管理所有租户),必须显式使用
TenantIgnoreHelper.ignore()包装,并在代码中注明安全意图 - 定时任务、事件监听器等异步场景在执行 DB 操作前,必须先设置
TenantContextHolder,执行完后在 finally 中清理