Dify 企业级实验(07):安全与合规——全链路脱敏与权限分级怎么做?
1. 业务场景
先讲一个我们实际遇到的场景。
客服系统每天处理用户消息,消息里带着手机号、身份证号、地址。这些数据一旦进 LLM 上下文,就出了企业边界——模型供应商、日志、缓存,每一处都是泄露口。
我们第一次接这类需求时,第一反应也是「脱敏嘛,回复之前把手机号打个码」。真正动手才发现——打完码再送进去,数据已经出过企业边界了;先清理永远追不上泄露,必须脱敏前置。个保法(PIPL)下,脱敏不是「最好做」,而是「必须做对」——而且是在设计上做对。
这不是个例。任何「处理用户隐私数据的客服/工单类应用」都是这个模式:手机号、身份证、地址,敏感信息只要进了 LLM,就等于外泄。
2. 场景痛点
这个流程的痛点,在合规负责人和客服主管身上体现得最直接:
- 数据外泄风险:手机号/身份证进 LLM 上下文,等于把用户隐私发给模型供应商,出了事责任全在己方。
- 事后清理不可靠:先送进 LLM、再从回复里删,漏删一次就是一次泄露——清理永远追不上泄露。
- 权限过大:一个 API Key 通吃所有应用,泄露即全暴露,撤销重建动静大。
- 审计缺失:脱敏做了没有、脱敏了几处,说不清楚,合规审查过不去。
本质上,脱敏前置 > 事后清理——敏感信息压根不该进 LLM 上下文。
3. 方案:为什么是脱敏前置
Dify 的 code 节点 + API Key 粒度 + 独立存储,正好组成「脱敏、分级、审计」三件套。选它的理由:
- 脱敏前置:正则识别 PII 并掩码,LLM 只见掩码文本——从源头掐断外泄;
- 双重保险:后置二次扫描兜底 + 受控恢复(只给内部回显场景),漏网概率降到最低;
- Key 分级 + 审计:每个外部系统独立 Key、最小授权,脱敏操作全程可审计。
这篇文章我们就用它搭一个「全链路脱敏客服」:前置掩码 + 后置扫描 + 审计日志。
4. 整体架构
链路很清晰:脱敏前置 → 审计入库 → 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. 实验文档及源码获取
- 实验文档:DIFY-104-07:安全与合规——全链路脱敏与权限分级.md
- 源码一(脱敏客服):dify104_07_01_脱敏客服.yml
- 源码目录:dify-104/dsl
文章聚焦核心配置与采坑点,完整分步操作与权限配置说明见实验文档原文。
- Dify 企业级实验(01):多应用编排——如何让多个 Dify 应用协同完成一条业务链?
- Dify 企业级实验(02):跨应用状态传递——多轮对话的状态如何跨应用不丢?
- Dify 企业级实验(03):事件驱动流水线——Webhook 与定时触发如何组成异步处理链?
- Dify 企业级实验(04):性能优化实战——长流程从 60 秒到秒回有哪些手段?
- Dify 企业级实验(05):Token 成本控制——AI 应用省钱改造怎么做?
- Dify 企业级实验(06):可观测性体系——日志埋点与监控告警如何落地?
- Dify 企业级实验(07):安全与合规——全链路脱敏与权限分级怎么做?
- Dify 企业级实验(08):人机协同审批流——机器预审与人工确认如何配合?
- Dify 企业级实验(09):复杂业务状态机——订单状态流转与非法跳转防护?
- Dify 企业级实验(10):知识库持续更新闭环——数据飞轮怎么转起来?
- Dify 企业级实验(11):企业 API 工具化——如何把客户系统封装成 Dify 工具?
- Dify 企业级实验(12):外部系统集成——第三方系统如何通过 Dify API 双向编排?