← 返回文章列表

Dify 企业级实验(07):安全与合规——全链路脱敏与权限分级怎么做?

1. 业务场景

先讲一个我们实际遇到的场景。

客服系统每天处理用户消息,消息里带着手机号、身份证号、地址。这些数据一旦进 LLM 上下文,就出了企业边界——模型供应商、日志、缓存,每一处都是泄露口。

我们第一次接这类需求时,第一反应也是「脱敏嘛,回复之前把手机号打个码」。真正动手才发现——打完码再送进去,数据已经出过企业边界了;先清理永远追不上泄露,必须脱敏前置。个保法(PIPL)下,脱敏不是「最好做」,而是「必须做对」——而且是在设计上做对。

这不是个例。任何「处理用户隐私数据的客服/工单类应用」都是这个模式:手机号、身份证、地址,敏感信息只要进了 LLM,就等于外泄。

2. 场景痛点

这个流程的痛点,在合规负责人和客服主管身上体现得最直接:

本质上,脱敏前置 > 事后清理——敏感信息压根不该进 LLM 上下文

3. 方案:为什么是脱敏前置

Dify 的 code 节点 + API Key 粒度 + 独立存储,正好组成「脱敏、分级、审计」三件套。选它的理由:

这篇文章我们就用它搭一个「全链路脱敏客服」:前置掩码 + 后置扫描 + 审计日志。

4. 整体架构

graph TD start["开始(用户消息)"] mask["脱敏前置(code:PII 掩码)"] audit["组装审计日志(code)"] write_log["写入审计日志(http/KV)"] llm["LLM 处理(只见脱敏文本)"] post["结果后置(code:二次扫描+受控恢复)"] end1["结束"] start --> mask --> audit --> write_log --> llm --> post --> end1

链路很清晰:脱敏前置 → 审计入库 → LLM 只见掩码文本 → 后置二次扫描 → 输出。敏感信息不进 LLM 上下文是这条链的核心原则,后置扫描只是双保险。

5. 模块设计

5.1 脱敏前置(PII 掩码)

脱敏前置 code 节点:正则识别三类 PII 并掩码,同时统计命中类型与次数(审计数据不含原始值):

def main(user_message: str) -> dict:

    import re

    text = user_message or ""

    audit = {"phone": 0, "id_card": 0, "email": 0}

    def m_phone(m):

        audit["phone"] += 1

        return m.group(1) + "****" + m.group(2)

    def m_id(m):

        audit["id_card"] += 1

        return "****" + m.group(1)

    def m_email(m):

        audit["email"] += 1

        return m.group(1) + "***@" + m.group(2)

    masked = re.sub(r"(1[3-9]\d)(\d{4})\d{4}", m_phone, text)

    masked = re.sub(r"\d{14}(\d{4})", m_id, masked)

    masked = re.sub(r"([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*@([a-zA-Z0-9.-]+)", m_email, masked)

    total = audit["phone"] + audit["id_card"] + audit["email"]

    return {"masked": masked, "masked_count": str(total),

            "audit": "脱敏 " + str(total) + " 处(手机号 " + str(audit["phone"])

                     + " / 身份证 " + str(audit["id_card"]) + " / 邮箱 " + str(audit["email"]) + ")"}

5.2 审计日志独立存储

审计日志独立存储:脱敏完成后先写审计日志(KV dify104_07_audit,记录类型/次数,不含原始值),再进 LLM——审计与业务日志分离是合规审查要求。

5.3 LLM 处理(只见脱敏文本)

LLM 处理:prompt 明确「你收到的是脱敏数据,不要推测原始信息」,上下文只含掩码文本。

5.4 结果后置(二次扫描 + 受控恢复)

结果后置 code 节点(二次扫描 + 受控恢复):

def main(masked: str, user_message: str, lm_text: str) -> dict:

    import re

    def m_phone(m):

        return m.group(1) + "****" + m.group(2)

    safe = re.sub(r"(1[3-9]\d)(\d{4})\d{4}", m_phone, lm_text or "")

    safe = re.sub(r"\d{14}(\d{4})", lambda m: "****" + m.group(1), safe)

    m = re.search(r"1[3-9]\d{9}", user_message or "")

    restored = m.group(0) if m else "未提供"

    return {"safe_output": safe, "restored_phone": restored}

对 LLM 输出再扫一遍未掩码 PII(双保险),restored_phone 是受控恢复演示——需要回显手机号的内部场景(如工单详情)才从原始输入取回,普通响应走 safe_output

5.5 权限分级(API Key 隔离)

权限分级:为每个外部系统单独创建 API Key,一个 Key 只授权对应应用(Dify API Key 粒度),Key 泄露可一键撤销重建轮换。

6. 运行验证

输入 预期 结果
含手机号 2 处 + 身份证 + 邮箱的消息 前置脱敏 3 处,LLM 只见脱敏文本 手机号 2 处 + 邮箱 1 处被掩码,LLM 回复不含任何明文 PII,后置二次扫描输出安全
内部回显场景 受控恢复手机号 restored_phone=13812345678(仅内部演示)
查询审计日志 记录脱敏类型/次数,不含原始值 KV 落 1 条审计记录
只读 Key 调写操作 拒绝 API Key 粒度隔离,越权调用被拒

环境:Dify 1.16.1(Docker Compose),模型 DeepSeek deepseek-v4-flash。DSL 导入发布通过,Service API 验证。

7. 实战坑

现象 修复
脱敏在 LLM 之后才做 敏感数据已进 LLM 上下文,等于外泄 脱敏前置是设计原则:LLM 只接触掩码文本,后置只做二次扫描兜底
正则漏识别格式变体 带空格/区号的手机号、新域名邮箱漏网 用 doc-cleaner 脱敏三策略(mask/redact/hash)+ 敏感词表补充,边界用例回归(实测)
code 节点沙箱禁写文件 审计日志写文件报 PermissionError(/tmp) 审计日志走 http 写 KV 模拟服务(dify104-kv,172.19.0.50:8123),生产换 Redis/DB,拓扑不变
Key 权限过大 一个 Key 所有应用可用,泄露即全暴露 每个集成方独立 Key + 最小授权范围 + 泄露一键撤销重建(102/103 Key 管理经验)
审计日志混入业务日志 合规审查困难 审计独立 KV key(dify104_07_audit),与业务日志分离存储

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与权限配置说明见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过