在 Go 的 slog 中脱敏敏感数据:使用 masq 的实用指南

发布: (2026年1月18日 GMT+8 08:24)
8 min read
原文: Dev.to

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 通过 slogReplaceAttr 选项进行挂钩,递归检查所有日志值——包括嵌套的结构体——以过滤敏感数据。

基本用法

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

  • 问题: slogLogValuer 无法对嵌套字段进行脱敏。
  • 解决方案: 使用 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 fieldsmasq 无法可靠地克隆嵌入的私有 map 类型;它们会变成 nil
    请改用结构体字段来存放敏感数据。

  • Performance – 深度检查会带来一定的开销。
    对于高吞吐系统,建议使用抽样或异步日志记录。

结论

防止日志中泄露敏感数据对于安全和合规至关重要。虽然 slogLogValuer 有助于直接值的处理,masq 将此保护扩展到嵌套结构体,并提供灵活的脱敏策略。

试一试 masq 并告诉我们你的想法!

如果你觉得这篇文章有帮助,欢迎在 GitHub 上给仓库点个 ⭐,或在下方评论区分享你的反馈。

Back to Blog

相关文章

阅读更多 »

Gin vs Spring Boot:详细比较

封面图片:Gin vs Spring Boot:详细比较 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%...