RAG为什么会检索到其他租户的文档?多租户权限泄露完整治理方案
文章摘要
企业RAG最严重的故障不是答案不准确,而是用户能够通过自然语言检索到其他租户、其他部门或更高密级的文档。常见根因包括租户ID来自请求参数、过滤只在生成后执行、Top K先召回再删权限、文档Metadata缺失、缓存Key不含租户和权限、重排器接触无权限内容等。本文给出从身份、入库、检索、缓存、重排、生成和审计七层构建权限闭环的完整方案。
一、权限泄露通常是怎么发生的
一个典型错误链路:
用户登录租户A
→ 提问“公司今年收入是多少”
→ 向量库在全量文档中召回
→ Top5中包含租户B财务报告
→ 应用层只检查最终答案
→ 模型引用租户B数据
或者系统在应用层删除无权限结果:
全库Top5
→ 删除4条无权限文档
→ 剩1条或0条
这不仅会降低召回质量,更重要的是:
无权限文档已经进入检索结果、日志、Trace、重排器或大模型上下文。
权限治理必须从候选集合产生之前开始。
二、第一原则:租户ID不能来自用户自由输入
错误接口:
{
"question": "查询经营数据",
"tenantId": "T002"
}
用户可以把自己的T001改为T002。
正确链路:
Access Token
→ 服务端验证签名
→ 解析user_id
→ 权限服务查询tenant_id和角色
→ 构造检索过滤器
Controller示例:
@PostMapping("/ask")
public AnswerResponse ask(
@AuthenticationPrincipal
LoginUser user,
@RequestBody
AskRequest request
) {
RetrievalPrincipal principal =
permissionService.resolve(user.id());
return ragService.answer(
principal,
request.question()
);
}
用户请求中可以有业务筛选,但不能覆盖服务端解析出的身份范围。
三、权限模型不要只设计一个tenant_id
企业RAG通常存在多层权限:
租户
组织
部门
项目
角色
文档密级
用户白名单
有效期
建议定义统一权限主体:
public record RetrievalPrincipal(
String userId,
String tenantId,
Set departmentIds,
Set projectIds,
Set roles,
int securityLevel,
String permissionVersion
) {
}
文档Metadata:
{
"tenant_id": "T001",
"department_ids": ["D01", "D02"],
"project_ids": ["P100"],
"allowed_roles": ["MANAGER"],
"security_level": 2,
"visibility": "DEPARTMENT",
"status": "EFFECTIVE"
}
四、权限过滤必须下推到向量数据库
错误方案:
先全库向量搜索
→ 再由Java过滤
正确方案:
服务端权限上下文
→ 生成数据库原生过滤条件
→ 在有权限候选中执行向量搜索
逻辑示例:
tenant_id = T001
AND status = EFFECTIVE
AND security_level metadata
) {
require(metadata, "tenant_id");
require(metadata, "visibility");
require(metadata, "security_level");
require(metadata, "status");
}
private void require(
Map metadata,
String key
) {
if (!metadata.containsKey(key)) {
throw new IllegalArgumentException(
"缺少权限字段:" + key
);
}
}
}
还需要保证同一文档的所有Chunk继承相同权限,不要出现:
正文Chunk有tenant_id
表格Chunk没有tenant_id
图片OCR Chunk没有tenant_id
七、权限变化后如何同步
员工转岗、项目结束或文档密级调整后,向量库权限必须及时更新。
推荐:
权限系统变更
→ 发布PermissionChanged事件
→ 更新向量库Payload
→ 清理缓存
→ 更新permission_version
每次查询记录:
permission_version
这样可以追踪某次结果使用的是哪一版权限快照。
对于高风险文档,可以采用查询时实时鉴权;对于大规模普通文档,可以使用权限Payload加短期版本缓存。
八、缓存是最容易遗漏的泄露点
错误缓存Key:
hash(question)
租户A问:
今年销售额是多少?
缓存了答案。
租户B提出相同问题,可能命中同一个缓存。
正确Key至少包括:
tenant_id
user_or_role_scope
permission_version
knowledge_version
question_hash
prompt_version
model
示例:
String cacheKey = String.join(
":",
principal.tenantId(),
principal.permissionVersion(),
knowledgeVersion,
sha256(question)
);
如果权限精确到用户,还要包含用户或权限集合哈希。
九、语义缓存也必须权限隔离
语义缓存会根据相似问题复用答案。
即使问题不完全相同,也可能命中:
A:华东区域本月销售如何?
B:这个月华东卖得怎么样?
语义缓存检索本身也必须带:
tenant_id
permission_hash
knowledge_version
不能建立一个全局答案向量库后跨租户搜索。
十、重排器能否看到无权限文档
错误链路:
全库召回Top100
→ Cross-Encoder重排
→ 过滤权限
即使最终结果过滤了,重排服务已经读取无权限内容。
正确顺序:
权限过滤
→ 召回
→ 融合
→ 重排
如果使用第三方Rerank API,还要确认:
- 文档是否允许发送给第三方;
- 数据是否保存;
- 是否用于训练;
- 区域和合规;
- 日志保留。
十一、LLM上下文前再做一次防御校验
数据库过滤是第一道防线,进入Prompt前可以再校验一次Metadata:
List authorized =
chunks.stream()
.filter(chunk ->
accessEvaluator.canRead(
principal,
chunk.metadata()
)
)
.toList();
这不是替代数据库过滤,而是纵深防御。
如果发现未授权Chunk:
阻断请求
记录安全事件
不继续调用模型
不要静默删除后继续回答,因为这说明前置权限链已经失效。
十二、生成阶段如何避免信息侧漏
模型可能根据对话历史复述旧内容。
需要确认:
- Chat Memory是否按租户和用户隔离;
- Conversation ID是否全局唯一;
- 历史消息是否含其他用户数据;
- 摘要是否跨用户复用;
- Prompt缓存是否隔离;
- 工具结果是否进入正确会话。
Memory Key推荐:
tenant_id
+user_id
+conversation_id
不能只使用:
conversation_id = 1
十三、日志和Trace也属于数据访问面
即使用户看不到,运维日志可能包含:
- 检索原文;
- 完整Prompt;
- 客户合同;
- 身份信息;
- 工具结果。
建议普通日志只记录:
document_id
chunk_id
score
metadata_hash
详细内容进入受控审计存储,并设置:
- 权限;
- 保留期;
- 脱敏;
- 下载审计;
- 删除机制。
十四、权限查询失败时应该怎样处理
错误策略:
权限服务超时
→ 不加过滤继续检索
这是Fail Open,会直接造成泄露。
高风险系统应Fail Closed:
权限无法确认
→ 拒绝检索
→ 返回稍后重试
同时区分:
权限不足
权限服务不可用
知识库无结果
十五、公开文档也要绑定租户吗
“公开”可能有不同范围:
互联网公开
平台所有租户公开
当前租户公开
组织内部公开
不要只用一个:
visibility = PUBLIC
建议:
PUBLIC_INTERNET
PUBLIC_PLATFORM
PUBLIC_TENANT
DEPARTMENT
PROJECT
PRIVATE
并明确每种范围的租户约束。
十六、自动化安全测试
构造两个租户:
T001文档包含秘密代码ALPHA-7788
T002用户无权限
测试:
- 直接问代码;
- 使用同义表达;
- 请求总结全部文档;
- 通过多轮对话诱导;
- 使用拼写错误;
- 请求列出来源;
- 命中语义缓存;
- 重排服务异常;
- 权限服务超时。
预期:
T002永远不能获得ALPHA-7788
十七、建议监控指标
unauthorized_candidate_count
permission_filter_error_count
missing_tenant_metadata_count
permission_service_timeout
cross_tenant_cache_hit
blocked_context_count
security_incident_count
任何:
unauthorized_candidate_count > 0
都应该作为安全告警,而不只是普通检索指标。
十八、生产级权限链
认证
→ 解析用户与租户
→ 获取权限快照
→ 构造数据库过滤
→ 在权限范围内召回
→ 融合与重排
→ 上下文二次校验
→ 调用模型
→ 结果审计
→ 权限隔离缓存
总结
多租户RAG权限泄露通常来自:
身份来源不可信
过滤执行太晚
Metadata缺失
缓存未隔离
重排器先看到全量文档
Memory跨用户复用
权限失败时Fail Open
企业RAG必须把权限设计成贯穿入库、召回、重排、生成、缓存和日志的完整链路,而不是在最终答案上增加一次字符串检查。
延伸阅读
如果你正在关注企业级 AI 应用、RAG、Agent、MCP 与大模型工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。