数据脱敏规则配置文档¶
文档版本: 1.0.3
最后更新: 2026-09-15
Git 提交: 61384b4a
作者:
概述¶
JAiRouter 的数据脱敏功能可以自动识别和处理响应与调用记录中的敏感信息,包括个人身份信息(PII)、敏感词汇等。
v3.2.0 起:记录侧主路径与控制台管理¶
主战场是「聊天调用历史 / 日志 / 追踪记录」中的用户数据,经记录侧 SanitizationService.sanitizeForStorage 处理;该路径不受网关 response.enabled 开关拖累(配置中的 PII/敏感词始终加载进规则库)。
| 能力 | 说明 |
|---|---|
| 控制台 | 安全 → PII 脱敏管理(/security/sanitization) |
| 权限 | security:sanitization:manage(ADMIN) |
| 管理 API | GET/PUT /api/config/sanitization、GET .../rules、POST .../test |
| 网关响应过滤器 | 默认关闭;/api/** 管理面与运维端点排除;/v1/** 的非流式 JSON 自 #560 起脱敏;SSE(text/event-stream)自 #560 第二阶段起逐帧脱敏(response.sanitize-sse,默认开) |
| 试脱敏 | 样例非 JSON 时按 text/plain 掩码,便于粘贴聊天原文 |
调用历史:record-level=SUMMARY 时,适配器层用 sanitizeForStorage 落库前脱敏。
管理 API 详见 管理 API 与控制台手册 web-console.md。
功能特性¶
- 响应与记录脱敏:网关出站响应(JSON / SSE)与调用历史、日志、追踪落库前都会脱敏
- 多种脱敏策略:支持掩码、替换、删除、哈希等策略
- 正则表达式支持:支持复杂的模式匹配
- 性能优化:支持并行处理和缓存优化
- 服务差异化:不同 AI 服务可以使用不同的脱敏规则
快速开始¶
1. 启用数据脱敏¶
2. 基础配置¶
jairouter:
security:
sanitization:
response:
enabled: true
sensitive-words:
- "password"
- "secret"
pii-patterns:
- "(?<!\\d)1[3-9]\\d{9}(?!\\d)" # 手机号(数字边界,避免从长数字串中间截取)
- "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" # 邮箱
masking-char: "*"
3. 测试脱敏效果¶
发送包含敏感信息的请求:
curl -H "X-API-Key: your-api-key" \
-X POST \
-H "Content-Type: application/json" \
-d '{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "我的手机号是13812345678,邮箱是user@example.com"}]}' \
http://localhost:8080/v1/chat/completions
当前版本不会改写发往 AI 模型的请求体,上面的请求会原样到达上游;响应侧规则在响应体与调用记录 / 日志落库前都会生效。启用响应脱敏后,返回内容中的手机号与邮箱会被打码,例如:
详细配置¶
⚠️ 本节的示例配置并非全部可用键。当前实现只绑定
response下的这些键:enabled、sensitive-words、pii-patterns、masking-char、log-sanitization、fail-on-error、preserve-json-structure、scope、max-buffered-bytes、sanitize-sse。 举例里出现的strategies(keep-prefix-suffix/keep-domain等)、rule-priorities、service-specific-rules尚未实现:写进 YAML 不会报错,但也不会生效。 实际可用的替换策略是规则上的strategy字段(MASK/REPLACE/REMOVE/HASH),默认规则统一用MASK(整段等长替换,例如 11 位手机号 →***********)。
响应数据脱敏配置¶
jairouter:
security:
sanitization:
response:
# 启用响应脱敏
enabled: true
# 响应敏感词
sensitive-words:
- "internal" # 内部信息
- "debug" # 调试信息
- "error" # 错误详情
- "exception" # 异常信息
# 响应 PII 模式(顺序 = 执行顺序,先长后短)
pii-patterns:
- "(?<!\\d)\\d{17}[\\dXx](?!\\d)"
- "(?<!\\d)(?:\\d{16}|\\d{19})(?!\\d)"
- "\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b"
- "(?<!\\d)1[3-9]\\d{9}(?!\\d)"
- "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
# 保持 JSON 结构
preserve-json-structure: true
# 生效范围(只作用于 HTTP 响应脱敏过滤器)
# mode=exclude(默认)时 patterns 是「不脱敏」的例外,留空则用内置排除集合;
# mode=include 时只有命中 patterns 的路径才脱敏,留空 = 一条都不脱敏(会打 WARN)。
scope:
mode: exclude
patterns:
- "/actuator"
- "/swagger-ui"
响应脱敏生效范围(scope)¶
响应脱敏按路径过滤,再按内容类型分流:application/json 整段脱敏;text/event-stream(SSE)逐帧脱敏(见下节);其余内容类型原样透传。默认排除集合覆盖管理台、Actuator、Swagger、静态资源等;/v1 的非流式 JSON 与 SSE 都在生效范围内。
jairouter:
security:
sanitization:
response:
scope:
mode: exclude # exclude(默认)| include
patterns: [] # 留空 = 用内置排除集合
exclude:patterns里的路径跳过脱敏,其余全脱敏;patterns为空时用内置集合(与未配置scope完全等价)。include:只有命中patterns的路径脱敏;patterns为空时一条都不脱敏,启动日志会 WARN。- 匹配规则:不含通配符按前缀匹配(
/actuator命中/actuator/health);含*/**按 Ant 风格匹配,/v1/**同时命中/v1本身。 - 该配置目前只读:控制台「PII 脱敏管理 → 响应脱敏生效范围」卡片会回显当前模式、来源(内置默认 / 配置指定)与规则列表,并提供路径预览(
POST /api/config/sanitization/scope/preview);修改需改配置文件并重启。 - 记录侧脱敏(调用历史 / 日志 / 追踪)不受该范围影响。
脱敏策略详解¶
1. 完全脱敏(full-mask)¶
将整个匹配内容替换为脱敏字符:
2. 保留前缀后缀(keep-prefix-suffix)¶
保留开头和结尾的部分字符:
3. 保留域名(keep-domain)¶
仅用于邮箱地址,保留域名部分:
4. 哈希脱敏(hash)¶
使用哈希函数替换敏感信息:
5. 替换脱敏(replace)¶
使用预定义的替换文本:
6. 删除脱敏(remove)¶
完全删除匹配的内容:
正则表达式模式¶
常用 PII 模式¶
中国手机号¶
pii-patterns:
- "\\b(?:13[0-9]|14[5-9]|15[0-3,5-9]|16[2,5,6,7]|17[0-8]|18[0-9]|19[1,3,5,8,9])\\d{8}\\b"
中国身份证号¶
pii-patterns:
- "\\b[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]\\b"
邮箱地址¶
银行卡号¶
IP 地址¶
pii-patterns:
- "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b"
URL 地址¶
pii-patterns:
- "https?://[\\w\\-]+(\\.[\\w\\-]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?"
自定义模式示例¶
员工工号¶
订单号¶
车牌号¶
服务差异化配置¶
不同的 AI 服务可以使用不同的脱敏规则:
jairouter:
security:
sanitization:
response:
service-specific-rules:
# 聊天服务
chat:
enabled: true
additional-patterns:
- "\\b(?:QQ|微信|WeChat)[::]?\\s*\\d+\\b"
- "\\b(?:支付宝|Alipay)[::]?\\s*[\\w\\d]+\\b"
strategies:
social-media: "keep-prefix-suffix"
# 向量服务(通常不需要脱敏)
embedding:
enabled: false
# 重排序服务
rerank:
enabled: true
preserve-ranking-scores: true
additional-patterns:
- "\\b(?:评分|得分|分数)[::]?\\s*\\d+(\\.\\d+)?\\b"
# 语音合成服务
tts:
enabled: true
additional-patterns:
- "\\b(?:声音|语音|音色)[::]?\\s*[\\w\\d]+\\b"
# 语音识别服务
stt:
enabled: true
preserve-timestamps: true
# 图像生成服务
image-generation:
enabled: true
additional-patterns:
- "\\b(?:风格|style)[::]?\\s*[\\w\\d]+\\b"
性能影响与容量评估¶
脱敏不是零成本的旁路:每一次命中都要对整段文本做正则匹配与文本重写。响应侧在出站(ResponseSanitizationFilter);此外记录侧(sanitizeForStorage,用于调用历史与链路追踪落库前)也执行同一套匹配。
当前实际覆盖面(按现状评估,不要按「全量覆盖」估算)¶
| 位置 | 生效点 | 现状 |
|---|---|---|
| 响应侧 | 出站响应体过滤器 | application/json 整段脱敏;text/event-stream(SSE)逐帧脱敏(自 issue #560 第二阶段起,response.sanitize-sse,默认开) |
| 响应侧路径 | 路径过滤 | /v1/** 的非流式 JSON 与 SSE 自 issue #560 起纳入脱敏;/api/**、/admin/**、/actuator/** 与静态资源仍在 ExcludedPathsConfig 中被默认排除 |
| 记录侧 | 落库前(sanitizeForStorage) | 受 record-level 与调用历史配置影响,与 response.enabled 无关 |
因此,打开响应侧开关后,容量评估要按三档算:① 所有 application/json 响应 —— 含 AI 实时推理主入口(/v1/chat/completions)的非流式返回 —— 都会整段过一次脱敏(issue #560 方案 C 第一阶段已把 /v1/** 的 JSON 放开);② SSE(text/event-stream)按帧脱敏,成本随帧数与单帧大小增长,但不需要整段缓冲,长连接不会把整个流读进内存(issue #560 第二阶段:逐行处理,data: 行替换、其它行与 [DONE] 原样透传);③ 管理面(/api/**)、运维端点与静态资源仍不改写。做容量评估时不要按「所有响应都被脱敏」来估算,也不要再按「/v1 全都不脱敏」或「SSE 不脱敏」来估算。
另外,JSON 模式下 traceId / spanId / requestId / traceparent / tracestate 这几个键的值默认原样保留(键名大小写不敏感):把排障标识打码会让响应体与日志对不上,属于「多破坏」(issue #561)。其它键的值照旧按规则脱敏,非 JSON 文本没有键名可推断、行为不变。
开销随什么增长¶
- 规则条数:每条启用规则都会对每段待检文本至少尝试一次匹配,成本大体按「规则条数 × 文本长度」叠加。
- 正则复杂度:含嵌套量词或大量可回溯分支的表达式,在长文本上可能退化成灾难性回溯,单次匹配就能消耗数十毫秒。
- 文本体积:过滤器会把整个响应体读入内存再重写,成本与字节数近似线性;MB 级响应体还会额外占用一份堆内存。
- 请求速率:单次开销在并发下放大为 CPU 占用上升与平均 / 尾部延迟抬高;
fail-on-error=true时失败重试会再放大一次。
粗略口径(仅用于初始估算,不作为结论)¶
- 单次小响应(< 10KB)+ 常用 3~5 条 PII 正则:开销通常在亚毫秒级(< 1 ms),相对模型推理耗时可忽略。
- MB 级响应体,或启用几十条复杂正则:开销会进入毫秒到数十毫秒量级,必须按实际压测结果决定是否开启、以及先开哪一侧。
上线建议¶
- 先在预发用真实流量压测:用生产样本(长会话、大响应体)对比开关前后的平均 / 尾部延迟与 CPU,再决定上线。
- 从最小规则集开始:只启用必需的 PII 模式,验证效果后逐条加,不要一次性搬入规则库里的全部示例。
- 避免灾难性回溯的正则:引擎侧有
SafeRegexValidator做危险模式拦截,但它不是性能保证,也不覆盖所有退化形态;自定义正则仍应用长文本自行验证最坏情况。 - 区分网关与记录侧:
response.enabled只控制 HTTP 响应过滤器;记录侧(调用历史 / 日志 / 追踪)始终按全部已启用规则脱敏,不随该开关关闭。 - 控制台会提示:「PII 脱敏管理」页打开开关前会先弹出二次确认(提示性能开销),启用期间配置卡顶部保持一条常驻黄色警示。
关闭时零开销¶
response.enabled 为 false 时,响应过滤器在入口直接放行(ResponseSanitizationFilter.filter() 的第一个判断),既不读响应体也不做正则匹配与文本重写;记录侧脱敏不受该开关控制。
注意「零开销」只对网关路径成立:记录侧(sanitizeForStorage)不检查这两个开关,规则始终加载,只要存在可用规则就会在落库前执行匹配(DefaultSanitizationService.loadRequestRules 的注释写明这一取舍)。关闭开关能消除出站响应开销,但不会消除记录侧开销 —— 若调用历史/trace 量大,规则条数仍会影响写入侧延迟。
性能优化¶
并行处理¶
正则表达式缓存¶
批量处理¶
3. Redis 缓存¶
4. 应用安全配置¶
创建 config/application-security.yml:
# 安全配置
security:
# API Key 配置
api-key:
enabled: true
header: X-API-Key
keys:
# 生产环境使用环境变量配置
- name: "prod-admin"
value: "${PROD_ADMIN_API_KEY:}"
- name: "prod-service"
value: "${PROD_SERVICE_API_KEY:}"
- name: "prod-readonly"
value: "${PROD_READONLY_API_KEY:}"
# JWT 配置
jwt:
enabled: true
secret: "${JWT_SECRET:${PROD_JWT_SECRET:}}"
algorithm: HS256
expiration-minutes: 15
issuer: jairouter-prod
accounts:
- username: admin
password: "{bcrypt}your-bcrypt-hashed-password"
roles: [ADMIN, USER]
enabled: true
- username: user
password: "{bcrypt}your-bcrypt-hashed-password"
roles: [USER]
enabled: true
# CORS 配置
cors:
allowed-origins: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: false
# HTTPS 配置
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: password
key-store-type: PKCS12
key-alias: jairouter
监控和审计¶
脱敏指标¶
jairouter:
security:
monitoring:
metrics:
sanitization:
enabled: true
histogram-buckets: [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]
审计日志¶
监控指标¶
jairouter_security_sanitization_operations_total:脱敏操作总数jairouter_security_sanitization_duration_seconds:脱敏操作耗时jairouter_security_sanitization_patterns_matched_total:匹配的模式总数jairouter_security_sanitization_bytes_processed_total:处理的字节总数
最佳实践¶
1. 规则设计¶
- 精确匹配:使用精确的正则表达式避免误匹配
- 性能考虑:避免过于复杂的正则表达式
- 优先级设置:合理设置规则优先级
- 测试验证:充分测试脱敏规则的效果
2. 性能优化¶
- 合理配置线程池:根据系统资源配置线程池大小
- 启用缓存:启用正则表达式缓存提升性能
- 流式处理:对大文件启用流式处理
- 监控性能:持续监控脱敏操作的性能影响
3. 响应缓冲上限¶
响应侧脱敏需要把整个响应体读进内存做正则替换,因此有一个硬上限 jairouter.security.sanitization.response.max-buffered-bytes(默认 1 MiB,下限 1 KiB):
- 带
Content-Length且超过上限:跳过脱敏、原样放行,同时打 WARN 并记审计事件 —— 宁可漏脱敏,也不把大响应读进堆; - chunked(无
Content-Length)且超过上限:原始字节已被消费、无法回放,按脱敏失败终止,由全局异常处理器渲染错误体,不会输出半截内容。
调大上限会线性抬高单请求的内存占用,按「并发数 × 上限」估算堆预算;调小则更多大响应落到「跳过脱敏」。该上限只作用于 HTTP 响应过滤器,不影响记录侧的 sanitizeForStorage。
4. SSE 流式响应逐帧脱敏¶
流式响应(text/event-stream)不能整段缓冲 —— 长连接可能持续输出数分钟,DataBufferUtils.join 会把整个流读进内存直到连接结束。因此 SSE 走逐帧路径(issue #560 第二阶段):
处理规则:
- 按行切分,只替换以
data:开头的行里data:之后的负载;event:/id:/retry:/ 注释行(:)与data: [DONE]哨兵行原样透传; - 分帧语义不变:行尾(
\n/\r\n)、空行分隔符、字段顺序都不改写; - 帧可以跨网络块:残行会拼到下一块再处理,不会把一帧 JSON 切断;
- 单帧失败不中断整条流:某一帧脱敏抛异常时回退该帧原文并记告警,后续帧继续处理;
- 单行上限沿用
max-buffered-bytes(下限 1 KiB):单行超过上限时该行切「直通模式」原样输出并记一次告警,不会截断字节,也不影响后续行。
代价与边界:成本从「每响应一次」变成「每帧一次」,帧多时 CPU 与延迟会小幅上升(单帧通常只有几十到几百字节);sanitize-sse=false 时 SSE 完全不过脱敏(等同 #560 之前的旧行为),不建议在需要脱敏的场景关闭。该开关与 scope 独立:路径被 scope 排除时,SSE 也不会脱敏。
5. 安全考虑¶
- 日志安全:确保脱敏日志本身不包含敏感信息
- 规则保密:保护脱敏规则配置的安全性
- 定期更新:根据新的数据类型更新脱敏规则
- 合规检查:确保脱敏规则符合相关法规要求
故障排除¶
常见问题¶
1. 脱敏不生效¶
可能原因: - 脱敏功能未启用 - 正则表达式不匹配 - 规则优先级问题
解决方案: 1. 检查脱敏功能是否启用 2. 测试正则表达式是否正确 3. 调整规则优先级
2. 性能问题¶
可能原因: - 正则表达式过于复杂 - 线程池配置不当 - 缓存未启用 - 处理数据量过大
解决方案: 1. 优化正则表达式 2. 调整线程池大小 3. 启用缓存功能 4. 启用流式处理
3. 误脱敏¶
可能原因: - 正则表达式过于宽泛 - 规则冲突 - 策略配置错误
解决方案: 1. 精确化正则表达式 2. 调整规则优先级 3. 修正脱敏策略
调试技巧¶
1. 启用详细日志¶
2. 测试正则表达式¶
使用在线工具测试正则表达式: - https://regex101.com/ - https://regexr.com/
3. 监控脱敏效果¶
# 查看脱敏日志
tail -f logs/security-audit.log | grep sanitization
# 查看脱敏指标
curl http://localhost:8080/actuator/prometheus | grep sanitization
示例配置¶
基础配置¶
jairouter:
security:
enabled: true
sanitization:
response:
enabled: true
sensitive-words: ["internal", "debug", "error"]
preserve-json-structure: true
高级配置¶
jairouter:
security:
sanitization:
response:
enabled: true
service-specific-rules:
chat:
additional-patterns:
- "\\b(?:QQ|微信|WeChat)[::]?\\s*\\d+\\b"
embedding:
enabled: false