Featured image of post Go 日志脱敏极简实现

Go 日志脱敏极简实现

本文介绍了 go-zero 框架 v1.9.0 版本新增的日志脱敏功能,通过实现 Sensitive 接口,开发者可以优雅地保护敏感数据,如密码、手机号等。

核心内容点:

  1. 介绍日志脱敏的必要性和传统方案的不足
  2. go-zero v1.9.0 通过 Sensitive 接口实现自动脱敏
  3. 提供嵌套结构、切片和条件脱敏的使用示例及最佳实践

源自 | kevwan微服务实践 2025-08-04 09:39

前言

在微服务架构中,日志记录是调试和监控系统的重要手段。然而,日志中常常包含用户密码、手机号、身份证号等敏感信息,一旦泄露就可能造成严重的安全问题。如何在保证日志调试功能的同时有效保护敏感数据,成为了每个开发者都需要面对的挑战。

go-zero 框架将在 v1.9.0 版本(8 月发布)中新增了日志脱敏功能(PR,5003),为开发者提供了一个优雅且易用的敏感数据保护方案。本文将深入介绍这一特性的设计思路、实现原理和使用方法。也期望收到大家的反馈,争取把好的建议整合到新版本里。

问题背景

在实际开发中,我们经常遇到以下场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type User struct {
    Name     string`json:"name"`
    Password string`json:"password"`
    Phone    string`json:"phone"`
    Email    string`json:"email"`
}

// 用户登录逻辑
func LoginHandler(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
    user := User{
        Name:     req.Username,
        Password: req.Password,
        Phone:    req.Phone,
        Email:    req.Email,
    }

    // 记录用户信息到日志,但密码等敏感信息会被记录下来
    logx.Infov(ctx, user)

    // ... 业务逻辑
}

在上述代码中,logx.Infov() 会将整个 user 对象记录到日志中,包括明文密码,这显然存在安全风险。

传统的解决方案通常有以下几种:

  • 手动处理:在记录日志前手动清空敏感字段

自定义日志方法:为每种数据类型编写专门的日志记录方法

使用第三方库:依赖外部脱敏库

这些方案都存在一定的局限性:要么增加了开发负担,要么缺乏统一性,要么引入了额外的依赖。

go-zero 的解决方案

go-zero v1.9.0 通过引入 Sensitive 接口,提供了一个轻量级且优雅的日志脱敏解决方案。

核心设计

1. Sensitive 接口

1
2
3
4
5
6
7
8
9
// Sensitive is an interface that defines a method for masking sensitive information in logs.
// It is typically implemented by types that contain sensitive data,
// such as passwords or personal information.
// Infov, Errorv, Debugv, and Slowv methods will call this method to mask sensitive data.
// The values in LogField will also be masked if they implement the Sensitive interface.
type Sensitive interface {
    // MaskSensitive masks sensitive information in the log.
    MaskSensitive() any
}

这个接口设计非常简洁,只包含一个方法 MaskSensitive(),返回脱敏后的数据。

2. 自动脱敏机制

1
2
3
4
5
6
7
8
9
// maskSensitive returns the value returned by MaskSensitive method,
// if the value implements Sensitive interface.
func maskSensitive(v any) any {
    if s, ok := v.(Sensitive); ok {
        return s.MaskSensitive()
    }

    return v
}

框架会自动检测日志内容是否实现了 Sensitive 接口,如果实现了就自动调用脱敏方法。

3. 日志输出层集成

在日志输出的核心方法 output() 中,框架增加了脱敏处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func output(writer io.Writer, level string, val any, fields ...LogField) {
    switch v := val.(type) {
    casestring:
        // 字符串长度截断逻辑
        maxLen := atomic.LoadUint32(&maxContentLength)
        if maxLen > 0 && len(v) > int(maxLen) {
            val = v[:maxLen]
            fields = append(fields, truncatedField)
        }
    case Sensitive:
        // 新增:敏感数据脱敏
        val = v.MaskSensitive()
    }

    // +3 for timestamp, level and content
    entry := make(logEntry, len(fields)+3)
    for _, field := range fields {
        // LogField 中的值也会被脱敏
        entry[field.Key] = maskSensitive(field.Value)
    }

    // ... 其他逻辑
}

这种设计的巧妙之处在于:

  • • 透明性:对现有代码几乎无侵入

  • • 全面性:不仅主要日志内容会被脱敏,LogField 中的值也会被处理

  • • 高效性:只在需要时才进行脱敏操作

使用示例

基础用法

让我们回到前面的用户登录示例,看看如何使用新的脱敏功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
type User struct {
    Name     string`json:"name"`
    Password string`json:"password"`
    Phone    string`json:"phone"`
    Email    string`json:"email"`
}

// 实现 Sensitive 接口
// 注意:(u User) 这样的值传递对值类型的对象和指针类型的对象都有效,
// 而 (u *User) 这样的指针传递只对指针类型的对象有效,对值类型的对象不生效
func (u User) MaskSensitive() any {
    return User{
        Name:     u.Name,
        Password: "******",  // 密码脱敏
        Phone:    maskPhone(u.Phone),  // 手机号脱敏
        Email:    maskEmail(u.Email),  // 邮箱脱敏
    }
}

// 手机号脱敏函数
func maskPhone(phone string)string {
    iflen(phone) < 7 {
        return phone
    }
    return phone[:3] + "****" + phone[len(phone)-3:]
}

// 邮箱脱敏函数
func maskEmail(email string)string {
    parts := strings.Split(email, "@")
    iflen(parts) != 2 {
        return email
    }
    username := parts[0]
    iflen(username) <= 2 {
        return email
    }
    return username[:1] + "***" + username[len(username)-1:] + "@" + parts[1]
}

// 使用示例
func LoginHandler(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
    user := User{
        Name:     req.Username,
        Password: req.Password,
        Phone:    req.Phone,
        Email:    req.Email,
    }

    // 现在这里会自动脱敏
    logx.Infov(ctx, user)
    // 输出: {"name":"alice","password":"******","phone":"138****1234","email":"a***e@example.com"}

    // LogField 中的敏感数据也会被脱敏
    logx.Infow(ctx, "user login", logx.LogField{Key: "user", Value: user},
        logx.LogField{Key: "ip", Value: "192.168.1.1"})
}

高级用法

1. 嵌套结构脱敏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Order struct {
    ID       string `json:"id"`
    UserInfo User   `json:"user_info"`
    Amount   int64  `json:"amount"`
}

func (o Order) MaskSensitive() any {
    return Order{
        ID:       o.ID,
        UserInfo: o.UserInfo.MaskSensitive().(User), // 嵌套脱敏
        Amount:   o.Amount,
    }
}

2. 切片脱敏

1
2
3
4
5
6
7
8
9
type UserList []User

func (ul UserList) MaskSensitive() any {
    masked := make(UserList, len(ul))
    for i, user := range ul {
        masked[i] = user.MaskSensitive().(User)
    }
    return masked
}

3. 条件脱敏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type AdminUser struct {
    User
    IsAdmin bool`json:"is_admin"`
}

func (au AdminUser) MaskSensitive() any {
    // 管理员可以看到更多信息
    if au.IsAdmin {
        return AdminUser{
            User: User{
                Name:     au.Name,
                Password: "******",
                Phone:    au.Phone, // 管理员可以看到完整手机号
                Email:    au.Email,
            },
            IsAdmin: au.IsAdmin,
        }
    }

    // 普通用户完全脱敏
    return AdminUser{
        User:    au.User.MaskSensitive().(User),
        IsAdmin: au.IsAdmin,
    }
}

实现原理深入分析

设计模式

go-zero 的日志脱敏功能采用了以下设计模式: - 策略模式:通过 Sensitive 接口,让每个类型自定义脱敏策略

装饰器模式:在不修改原有日志逻辑的基础上,增加脱敏功能

模板方法模式:框架提供统一的脱敏流程,具体脱敏逻辑由业务代码实现

性能考虑

脱敏功能的性能影响非常小:

  • 接口检查开销:Go 的接口类型断言是高效的 O(1) 操作

按需执行:只有实现了 Sensitive 接口的类型才会执行脱敏

无额外内存分配:脱敏过程复用现有的日志处理流程

最佳实践建议

1. 脱敏策略设计

  • • 一致性:同类型的敏感数据保持相同的脱敏策略

  • • 可读性:脱敏后的数据应该保持一定的可读性,便于调试

  • • 安全性:确保脱敏程度足够,不会泄露原始信息

2. 团队规范

  • • 统一接口:团队内部定义统一的脱敏接口规范

  • • 代码审查:确保敏感数据结构都实现了脱敏接口

  • • 测试要求:为脱敏功能编写专门的测试用例

总结

go-zero v1.9.0 的日志脱敏功能通过简洁的接口设计和巧妙的实现,为开发者提供了一个优雅的敏感数据保护方案。这个功能具有以下优势: - 简单易用:只需实现一个接口方法

性能优异:几乎零性能开销

扩展性强:支持各种复杂的脱敏场景

透明集成:对现有代码无侵入

对于使用 go-zero 框架的开发者来说,这个功能是数据安全防护的重要工具。建议在处理用户敏感信息的微服务中积极采用,为系统安全添加一道重要防线。

项目地址

https://github.com/zeromicro/go-zero

欢迎使用 go-zero 并 star 支持我们!