在 Go 的 slog 中脱敏敏感数据:使用 masq 的实用指南
I’m happy to translate the article for you, but I need the text of the post itself. Could you please paste the content you’d like translated (excluding the source link you’ve already provided)? Once I have the article text, I’ll translate it into Simplified Chinese while preserving the original formatting, markdown, and code blocks.
问题:日志中的敏感数据泄露
考虑一个典型场景:你正在为调试目的记录一个 User 结构体。
type User struct {
ID string
Email string
APIToken string
}
func main() {
user := User{
ID: "u123",
Email: "alice@example.com",
APIToken: "sk-secret-token-12345",
}
slog.Info("user logged in", "user", user)
}
输出
level=INFO msg="user logged in" user="{ID:u123 Email:alice@example.com APIToken:sk-secret-token-12345}"
哎呀。 API 令牌现在已经出现在日志中,可能被任何有日志访问权限的人看到,日志会被保存数月甚至数年,几乎无法删除。
slog的内置解决方案:LogValuer
Go 的 slog 包(自 Go 1.21 起成为标准库)提供了 LogValuer 接口,用于自定义值在日志中的显示方式:
type APIToken string
func (APIToken) LogValue() slog.Value {
return slog.StringValue("[REDACTED]")
}
func main() {
token := APIToken("sk-secret-token-12345")
slog.Info("token received", "token", token)
}
输出
level=INFO msg="token received" token=[REDACTED]
这对直接值有效。然而,LogValuer 在结构体字段上不起作用:
type APIToken string
func (APIToken) LogValue() slog.Value {
return slog.StringValue("[REDACTED]")
}
type Credentials struct {
UserID string
Token APIToken
}
func main() {
creds := Credentials{
UserID: "u123",
Token: "sk-secret-token-12345",
}
slog.Info("credentials", "creds", creds)
}
输出(令牌被暴露!)
level=INFO msg=credentials creds="{UserID:u123 Token:sk-secret-token-12345}"
当你记录一个结构体时,slog 使用反射并跳过嵌套字段上的 LogValue() 方法。此限制使得在实际应用中保护敏感数据变得困难。
masq: 自动深度脱敏
masq 通过 slog 的 ReplaceAttr 选项进行挂钩,递归检查所有日志值——包括嵌套的结构体——以过滤敏感数据。
基本用法
package main
import (
"log/slog"
"os"
"github.com/m-mizutani/masq"
)
type EmailAddr string
type User struct {
ID string
Email EmailAddr
}
func main() {
// 创建一个带有 masq 脱敏功能的 logger
logger := slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
}),
)
user := User{
ID: "u123",
Email: "alice@example.com",
}
logger.Info("user registered", "user", user)
}
示例输出
{
"time": "2026-01-18T12:00:00.000Z",
"level": "INFO",
"msg": "user registered",
"user": {
"ID": "u123",
"Email": "[FILTERED]"
}
}
电子邮件地址会被自动过滤,即使它位于嵌套结构体内部。
数据脱敏策略
masq 提供了多种识别并脱敏敏感数据的方式。
1. 按自定义类型
将敏感数据定义为不同的类型并进行脱敏:
type Password string
type CreditCard string
masq.New(
masq.WithType[Password](),
masq.WithType[CreditCard](),
)
2. 按结构体标签
使用结构体标签标记敏感字段:
type User struct {
ID string
Password string `masq:"secret"`
SSN string `masq:"secret"`
}
masq.New(masq.WithTag("secret"))
3. 按字段名
目标特定字段名:
masq.New(
masq.WithFieldName("Password"),
masq.WithFieldName("APIKey"),
)
4. 按字段前缀
脱敏所有以给定前缀开头的字段:
type Config struct {
SecretKey string // redacted
SecretToken string // redacted
PublicEndpoint string // not redacted
}
masq.New(masq.WithFieldPrefix("Secret"))
5. 按正则表达式模式
将值与正则表达式匹配(适用于信用卡号、电话号码等):
import "regexp"
// 脱敏潜在的信用卡号(16 位数字)
cardPattern := regexp.MustCompile(`\b\d{16}\b`)
masq.New(masq.WithRegex(cardPattern))
6. 按字符串内容
脱敏任何包含特定子串的值:
masq.New(masq.WithContain("Bearer "))
Source:
合并多种策略
您可以组合任意数量的脱敏规则:
logger := slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: masq.New(
// 按类型脱敏
masq.WithType[Password](),
masq.WithType[APIToken](),
// 按标签脱敏
masq.WithTag("secret"),
// 按字段名脱敏
masq.WithFieldName("SSN"),
// 按前缀脱敏
masq.WithFieldPrefix("Secret"),
// 按正则脱敏
masq.WithRegex(regexp.MustCompile(`\b\d{16}\b`)),
// 按字符串内容脱敏
masq.WithContain("Bearer "),
),
}),
)
现在每条日志条目都会经过所有配置的过滤器,确保没有敏感信息泄漏——即使它们深度嵌套也不例外。
TL;DR
- 问题:
slog的LogValuer无法对嵌套字段进行脱敏。 - 解决方案: 使用 masq 递归检查并过滤日志值。
- 做法: 将
masq.New(...)插入slog.HandlerOptions.ReplaceAttr,并选择一种或多种识别策略(类型、标签、字段名、前缀、正则或字符串内容)。
试试看,让您的日志保持干净且合规!
实际案例
以下是一个完整示例,展示了 masq 在典型 Web 应用场景中的使用:
package main
import (
"log/slog"
"os"
"regexp"
"github.com/m-mizutani/masq"
)
type (
Password string
AuthToken string
)
type LoginRequest struct {
Username string
Password Password
}
type UserSession struct {
UserID string `masq:"pii"`
AuthToken AuthToken
Email string `masq:"pii"`
PhoneNumber string
}
func main() {
// Configure comprehensive redaction
logger := slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: masq.New(
masq.WithType[Password](),
masq.WithType[AuthToken](),
masq.WithTag("pii"),
masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{10,14}$`)), // phone numbers
),
}),
)
slog.SetDefault(logger)
// Simulate login flow
req := LoginRequest{
Username: "alice",
Password: "super-secret-password",
}
slog.Info("login attempt", "request", req)
session := UserSession{
UserID: "u123",
AuthToken: "tok_abc123xyz",
Email: "alice@example.com",
PhoneNumber: "+14155551234",
}
slog.Info("session created", "session", session)
}
输出
{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"login attempt","request":{"Username":"alice","Password":"[FILTERED]"}}
{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"session created","session":{"UserID":"u123","AuthToken":"[FILTERED]","Email":"[FILTERED]","PhoneNumber":"[FILTERED]"}}
所有敏感数据都会被自动过滤,同时保留结构和非敏感字段。
最佳实践
- 为敏感数据定义自定义类型 – 与其使用普通的
string来存储密码或令牌,不如创建独立的类型(例如type Password string)。这使得脱敏操作明确,并能在编译时捕获问题。 - 对外部数据使用结构体标签 – 在处理第三方结构体或数据库模型时,添加
masq:"secret"标签以标记敏感字段。 - 谨慎使用正则表达式模式 – 正则匹配会对每个字符串值进行检查,因此应使用具体的模式以避免性能问题。
- 测试你的脱敏 – 编写测试以验证日志输出中不出现敏感数据。
- 默认进行脱敏 – 有疑虑时就进行脱敏。移除脱敏要比清理泄露的数据更容易。
限制
-
Private map fields –
masq无法可靠地克隆嵌入的私有 map 类型;它们会变成nil。
请改用结构体字段来存放敏感数据。 -
Performance – 深度检查会带来一定的开销。
对于高吞吐系统,建议使用抽样或异步日志记录。
结论
防止日志中泄露敏感数据对于安全和合规至关重要。虽然 slog 的 LogValuer 有助于直接值的处理,masq 将此保护扩展到嵌套结构体,并提供灵活的脱敏策略。
试一试 masq 并告诉我们你的想法!
如果你觉得这篇文章有帮助,欢迎在 GitHub 上给仓库点个 ⭐,或在下方评论区分享你的反馈。