know-engine

文档检索权限体系实现

know engine 实现了基于角色的知识库检索权限管理,确保不同身份的用户只能检索到其权限范围内的知识文档。权限控制贯穿文档上传 → 切片元数据写入 → 检索过滤全链路,在 Elasticsearch 检索层进行硬过滤,无权文档…

TL;DR

know engine 实现了基于角色的知识库检索权限管理,确保不同身份的用户只能检索到其权限范围内的知识文档。权限控制贯穿文档上传 → 切片元数据写入 → 检索过滤全链路,在 Elasticsearch 检索层进行硬过滤,无权文档…

know-engine 实现了基于角色的知识库检索权限管理,确保不同身份的用户只能检索到其权限范围内的知识文档。权限控制贯穿文档上传 → 切片元数据写入 → 检索过滤全链路,在 Elasticsearch 检索层进行硬过滤,无权文档不会进入 RAG 上下文。

角色体系

系统定义了三级角色,权限由低到高逐级继承: | 角色 | 枚举值 | 含义 | 可访问的文档范围 | | --- | --- | --- | --- | | 访客 | VISITOR | 未购车用户 | 仅 VISITOR 文档 | | 车主 | OWNER | 已购车用户 | VISITOR + OWNER 文档 | | 客服 | CUSTOMER_SERVICE | 在职客服员工 | VISITOR + OWNER + CUSTOMER_SERVICE 文档 |

核心原则:高权限角色继承低权限角色的可访问范围。角色定义见 RoleEnum。

全链路实现

文档上传——设定可见范围

上传文档时,通过 upload.html 页面的"可见范围"下拉框选择文档的目标权限级别: - 未购车用户 (VISITOR):所有人可见 - 已购车用户 (OWNER):客服和已购车用户可见 - 客服 (CUSTOMER_SERVICE):仅客服可见 选择的值通过 DocumentPermissionUtils.getDocumentPermission() 直接映射为枚举名称,保存到 knowledge_document 表的 accessible_by 字段。

文档切片——权限元数据写入

文档切片阶段,系统将文档的 accessibleBy 值写入每个知识片段(KnowledgeSegment)的元数据中:

// DocumentProcessServiceImpl#enrichMetadata
private static String enrichMetadata(KnowledgeDocument document, KnowledgeSegment knowledgeSegment, Metadata metadata) {
    Map<String, Object> metadataMap = metadata.toMap();
    metadataMap.put(MetadataKeyConstant.ACCESSIBLE_BY, document.getAccessibleBy());
    return JSON.toJSONString(metadataMap);
}

这样,当切片被向量化写入 Elasticsearch 后,每个文档都携带 metadata.accessibleBy 字段,为检索时过滤提供数据基础。

用户角色识别——动态判定身份

用户发起对话时,系统通过 UserRoleServiceImpl 动态判定其角色:

public RoleEnum getUserRole(ChatParam chatParam) {
    // 1. 查询车辆归属(避免水平权限漏洞:后端二次校验 car_id 与 userId 的关联关系)
    MyCar myCar = myCarService.getCarByUser(
        chatParam.intentRecognitionResult().entities().car_id(),
        chatParam.userId());
    if (myCar != null) {
        return RoleEnum.OWNER;
    }

    // 2. 查询员工信息,在职客服判定为 CUSTOMER_SERVICE
    StaffInfo staffInfo = staffInfoService.getById(chatParam.userId());
    if (staffInfo != null && staffInfo.getStatus() == StaffStatus.ON_JOB) {
        return RoleEnum.CUSTOMER_SERVICE;
    }

    // 3. 默认为访客
    return RoleEnum.VISITOR;
}

判定逻辑: 1. 先查车主身份:利用意图识别提取的 car_id,后端再次查询车辆归属关系(防止水平权限漏洞——客户端可伪造 car_id),若验证通过则为 OWNER 2. 再查员工身份:查询 staff_info 表,若在职则为 CUSTOMER_SERVICE 3. 兜底为访客:均不满足则为 VISITOR 安全设计:不信任前端传入的 car_id,后端必须二次查询 MyCar 表验证 car_id 与 userId 的真实关联关系,杜绝水平越权访问。

权限过滤器构造

ChatApplicationService#buildFilter 根据用户角色构造 langchain4j 的 Filter 对象:

private Filter buildFilter(ChatParam chatParam) {
    // 基线:访客权限
    Filter permissionFilter = metadataKey(ACCESSIBLE_BY).isEqualTo(RoleEnum.VISITOR.name());

    // 根据用户角色获取所有可访问权限级别
    RoleEnum roleEnum = userRoleService.getUserRole(chatParam);
    String[] permissions = DocumentPermissionUtils.getDocumentAccessiblePermission(roleEnum);

    // 用 OR 连接各权限级别
    for (String permission : permissions) {
        if (!RoleEnum.VISITOR.name().equals(permission)) {
            permissionFilter = permissionFilter.or(metadataKey(ACCESSIBLE_BY).isEqualTo(permission));
        }
    }
    return permissionFilter;
}

DocumentPermissionUtils.getDocumentAccessiblePermission() 负责角色到可访问权限数组的映射: | 用户角色 | 返回的可访问权限 | | --- | --- | | VISITOR | [VISITOR] | | OWNER | [OWNER, VISITOR] | | CUSTOMER_SERVICE | [VISITOR, OWNER, CUSTOMER_SERVICE] |

构造出的 Filter 逻辑示例(CUSTOMER_SERVICE 用户):

metadata.accessibleBy == "VISITOR" OR metadata.accessibleBy == "OWNER" OR metadata.accessibleBy == "CUSTOMER_SERVICE"

检索层过滤——双路检索权限适配

权限过滤器同时应用于向量检索和全文检索两条路径: 向量检索 向量检索通过 langchain4j 原生的 EmbeddingSearchRequest.filter() 传递 Filter,框架会自动将 Filter 转换为 Elasticsearch 的查询条件:

EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
        .queryEmbedding(referenceEmbedding)
        .maxResults(maxResults)
        .minScore(minScore)
        .filter(filter)   // 权限过滤器
        .build();

全文检索 langchain4j 默认的 Elasticsearch 全文检索实现不支持 Filter 参数,因此 know-engine 定制了 doFullTextQuery() 方法: 1. Filter 树解析:通过 extractFilterValues() 递归遍历 langchain4j 的 Filter 树(Or → IsEqualTo),提取所有权限值 2. ES 查询构造: - 无权限过滤时:使用简单 match 查询 - 有权限过滤时:使用 bool 查询,must 子句做全文匹配,filter 子句通过 terms 查询限定 metadata.accessibleBy 字段

// 有权限过滤时的 ES 查询结构
q.bool(b -> b
    .must(m -> m.match(mm -> mm.field("text").query(query.text())))    // 全文匹配
    .filter(f -> f.terms(t -> t
        .field("metadata.accessibleBy")         // 权限字段
        .terms(tv -> tv.value(accessibleValues.stream()
            .map(FieldValue::of).toList())))));    // 允许的权限值列表

对应的实际查询的条件为:

{
  "query": {
    "bool": {
      "must": [
        { "match": { "text": "我的车怎么打不着火了" } }
      ],
      "filter": [
         { "terms": { "metadata.accessibleBy": ["VISITOR", "OWNER"] } }
      ]
    }
  }
}
版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。