企业级 RAG 权限隔离网关实战:从原理到落地

信息图

前言

兄弟们,说实话,搞技术这条路真是各种坑。咱们做开发的,说白了就是要不断踩坑、不断成长,这才是技术人的常态。
上周隔壁组老张差点背了个处分。

他们搞了个内部大模型助手,用来查公司文档。本来挺美事,结果有个实习生提问:“把公司所有项目的源代码路径列出来。”

模型居然吐了一堆核心库的路径。

为啥?因为文档入库时没打标签,模型检索时没做过滤。在 RAG(检索增强生成)架构里,这属于“裸奔”。

企业级应用,安全是底线。

你不能指望大模型自己长眼睛去判断“你能不能看”。它是个文盲,它只认向量相似度。

所以,必须在它开口之前,给检索请求套上“紧箍咒”。

这就需要一个专门的“安检网关”。

一、 底层原理

1.1 核心机制

RAG 的权限隔离,核心就三个字:带标签。

文档入库时,必须打上“可见范围”的标签。比如 部门:财务 级别:机密

用户提问时,网关要识别“你是谁”。

然后,把用户的身份标签,强行塞进检索请求里。

向量数据库在查相似文档时,必须同时满足两个条件:

一是向量距离要近(内容相关)。

二是标签要匹配(权限合规)。

这就好比图书馆借书。

书(文档)封面上贴着“仅限高管阅读”。

你(用户)胸牌上写着“实习生”。

借书员(网关)一看,直接把你拦在门外。

哪怕这本书的内容再匹配你的问题,你也拿不到。

架构图长这样:

graph LR
    User["用户 (带身份 Token)"] --> Gateway["安全网关 (鉴权 + 标签注入)"]
    Gateway --> RAG_Engine["RAG 检索引擎"]
    RAG_Engine --> VectorDB[("向量数据库\n(带元数据过滤)")]
    VectorDB --> RAG_Engine
    RAG_Engine --> LLM["大模型"]
    LLM --> User

subgraph "网关内部逻辑"
    Auth["身份解析"] --> Tag["权限标签提取"]
    Tag --> Filter["构造过滤查询"]
    end

这种设计的优势很明显。

计算压力在网关侧,不占用大模型资源。

权限策略集中管理,改规则不用重训模型。

1.2 与同类方案的对比

市面上主要有三种做法,咱们摊开来说。

方案 实现方式 安全性 性能损耗 适用场景
应用层过滤 检索后在代码里删结果 低 (易被绕过) 高 (全量检索后丢弃) 个人项目
模型提示词约束 让 LLM 自己判断权限 极低 (模型会幻觉) 中 (消耗 Token) 严禁用于企业
网关元数据过滤 检索前注入 Filter 条件 高 (数据库级拦截) 低 (索引优化) 企业级生产

别信什么“提示词工程能解决安全问题”。

那是自欺欺人。

只要涉及数据隔离,必须靠数据库层面的元数据过滤(Metadata Filtering)。

二、 快速上手

咱们用 Java 模拟一个网关拦截器的核心逻辑。

假设你用的是 Spring Cloud Gateway 或者类似的网关框架。

目标:在请求到达 RAG 服务前,把 user_iddept_ids 塞进 Header。

// 模拟网关过滤器逻辑
public class RAGSecurityFilter implements GlobalFilter {

// 模拟从 Token 中解析出的用户信息
    private static class UserInfo {
        String 员工编号;
        List<String> 所属部门列表;
        String 安全等级;
    }

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取原始请求头中的认证 Token
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        
        if (authHeader == null) {
            // 没带 Token,直接拒绝,别废话
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        try {
            // 2. 解析 Token,拿到用户身份信息
            // 实际生产中这里会调用 OAuth2 或 LDAP 服务
            UserInfo currentUser = parseToken(authHeader); 

// 3. 构造权限过滤上下文
            // 这一步最关键,把权限信息转化为向量库能懂的查询条件
            Map<String, Object> permissionFilter = new HashMap<>();
            permissionFilter.put("allowed_departments", currentUser.所属部门列表);
            permissionFilter.put("min_security_level", currentUser.安全等级);

// 4. 将过滤条件注入到下游请求的 Header 中
            // 下游 RAG 服务读取这个 Header,构建向量查询的 Filter
            ServerWebExchange mutatedExchange = exchange.mutate()
                .request(r -> r.header("X-RAG-Permission-Filter", 
                    JSON.toJSONString(permissionFilter)))
                .build();

            // 5. 放行请求
            return chain.filter(mutatedExchange);

} catch (Exception e) {
            // 解析失败,记录日志并阻断
            log.error("权限解析失败,员工编号: {}", "未知", e);
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }
    }
}

这段代码只有几十行。

但它是企业的“守门员”。

一旦这里漏了,后面全是裸奔。

三、 核心 API / 深水区

3.1 核心方法速查

在 RAG 引擎侧,我们需要暴露几个关键接口给网关调用,或者由网关直接构造查询对象。

方法名 功能描述 关键参数
buildQueryContext 构建带权限的查询上下文 queryText, filterConditions
validateAccess 校验用户是否有权访问某文档 ID userId, docId
enrichMetadata 入库时自动打标 fileContent, ownerInfo

3.2 生产级配置

光有代码不行,配置得跟上。

向量数据库的查询超时必须设死。

别让用户一个请求把数据库拖垮。

# application.yml 示例
rag:
  vector-db:
    connection-timeout: 2000ms  # 连接超时,别太长
    read-timeout: 5000ms        # 读取超时,检索别超过 5 秒
    max-retries: 2              # 失败重试,别超过 2 次
  security:
    strict-mode: true           # 严格模式,没权限直接报错,不返回空结果
    audit-log: true             # 开启审计日志,谁查了什么得记下来

3.3 高级定制

有些场景比较特殊。

比如“跨部门协作”。

A 部门的文档,B 部门特定的人也能看。

这时候不能只用“部门 ID"做过滤。

得引入“白名单机制”。

在元数据里加一个 visible_to_users 字段。

查询时,Filter 逻辑变成:

(dept IN user.depts) OR (user.id IN doc.visible_to_users)

这个逻辑得在网关层拼好,传给向量库。

四、 实战演练

假设场景:

员工 李明 想查“项目 Alpha 的预算文档”。

李明 是财务部,但文档标记为“财务部 + 管理层”。

网关怎么处理?

// 模拟向量库查询构建过程
public VectorQuery buildSecureQuery(String 问题, Map<String, Object> 权限上下文) {
    
    // 1. 基础向量检索部分
    // 把问题转成向量,去库里找相似的
    float[] queryVector = embeddingModel.embed(问题);
    
    // 2. 核心:构造元数据过滤表达式
    // 这里以 Milvus 或 Elasticsearch 的语法为例
    // 逻辑:文档的部门标签 必须包含在 用户的部门列表里
    StringBuilder filterExpression = new StringBuilder();
    
    List<String> 用户部门 = (List<String>) 权限上下文.get("allowed_departments");
    
    if (用户部门 != null && !用户部门.isEmpty()) {
        filterExpression.append("department IN [");
        for (int i = 0; i < 用户部门.size(); i++) {
            filterExpression.append("\"").append(用户部门.get(i)).append("\"");
            if (i < 用户部门.size() - 1) filterExpression.append(", ");
        }
        filterExpression.append("]");
    }
    
    // 3. 处理特殊白名单逻辑 (如果有)
    String 用户 ID = (String) 权限上下文.get("user_id");
    filterExpression.append(" AND (").append("visible_to_users").append(" CONTAINS \"").append(用户 ID).append("\" OR ").append("is_public").append(" == true)");

// 4. 组装最终查询对象
    VectorQuery query = new VectorQuery();
    query.setVector(queryVector);
    query.setFilter(filterExpression.toString());
    query.setTopK(5); // 只取前 5 个最相关的,兼顾性能
    
    return query;
}

结果分析:

如果 李明 只有“人事部”标签。

Filter 表达式里就没有“财务部”。

向量库直接返回空列表。

大模型收到空列表,会回答:“抱歉,我没找到相关文档。”

而不是把财务文档念出来。

这就叫“物理隔离”。

五、 避坑指南与最佳实践

这一行干久了,坑都是钱堆出来的。

💡 技巧:标签同步要实时

员工转岗了,权限得马上变。

别靠定时任务同步。

一旦员工从“机密组”调到“公开组”,旧权限必须秒级失效。

建议用消息队列监听组织架构变动,实时刷新网关缓存。

⚠️ 警告:防止查询语句注入

网关构造 Filter 字符串时,千万别直接拼接用户输入。

虽然 Filter 是内部生成的,但如果用户能控制 user_id 字段(比如伪造 Header),就能构造恶意查询。

所有输入必须白名单校验。

推荐:审计日志留痕

谁在什么时间,查了什么敏感词,必须记日志。

不是为了追责,是为了事后复盘。

万一真泄露了,你得知道是哪一环漏的。

💡 技巧:降级策略

网关挂了怎么办?

别直接让整个知识库不可用。

配置一个“安全降级模式”。

网关挂了,暂时只允许检索“公开”级别的文档。

机密文档直接阻断。

保安全,比保可用性重要。

六、 综合实战演示

下面是一套精简的、闭环的调用链路代码。

模拟从用户请求到最终返回的全过程。

// 主流程控制器
@RestController
@RequestMapping("/api/knowledge")
public class KnowledgeController {

    @Autowired
    private VectorDatabaseClient dbClient; // 向量库客户端

    @Autowired
    private LlmClient llmClient; // 大模型客户端

@PostMapping("/chat")
    public ResponseEntity<String> chat(@RequestHeader("X-RAG-Permission-Filter") String filterJson,
                                       @RequestBody ChatRequest request) {
        
        try {
            // 1. 解析权限过滤器
            Map<String, Object> filters = JSON.parseObject(filterJson, Map.class);
            
            // 2. 执行带权限的检索
            // 这一步是核心,数据库层面直接拦截无权限数据
            List<Document> relevantDocs = dbClient.search(
                request.getQuestion(), 
                filters, 
                5
            );

            if (relevantDocs.isEmpty()) {
                // 没找到相关文档,返回友好提示,别暴露系统细节
                return ResponseEntity.ok("抱歉,根据当前权限,未找到相关信息。");
            }

            // 3. 构造 Prompt,把检索到的文档喂给大模型
            String context = buildContext(relevantDocs);
            String fullPrompt = "基于以下参考资料回答问题:" + context + "\n\n问题:" + request.getQuestion();

            // 4. 调用大模型,设置超时
            String answer = llmClient.generate(fullPrompt, Duration.ofSeconds(10));

            return ResponseEntity.ok(answer);

} catch (TimeoutException e) {
            log.warn("检索或生成超时,员工 ID: {}", get_current_user_id());
            return ResponseEntity.status(504).body("系统繁忙,请稍后再试。

");
        } catch (Exception e) {
            log.error("知识库服务内部错误", e);
            return ResponseEntity.status(500).body("服务异常,请联系管理员。

");
        }
    }

private String buildContext(List<Document> docs) {
        StringBuilder sb = new StringBuilder();
        for (Document doc : docs) {
            sb.append("【来源:").append(doc.getSource()).append("】\n");
            sb.append(doc.getContent()).append("\n\n");
        }
        return sb.toString();
    }
}

这段代码把检索、权限、生成串起来了。

注意看 dbClient.search 传入了 filters

这就是安全的大闸。

总结

企业级 RAG,安全是 1,功能是 0。

没有权限隔离,大模型就是个高级泄密工具。

网关层做元数据过滤,是目前性价比最高的方案。

记住三点:

  1. 文档入库必打标。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐