0

Bài 5: Tùy biến slog.Handler (Che giấu dữ liệu nhạy cảm)

Trong các hệ thống thực tế (như hệ thống xử lý giao dịch hay quản lý nhân sự), việc bảo vệ dữ liệu nhạy cảm là quy định bắt buộc (theo các chuẩn PCI-DSS hay GDPR). Nếu bạn vô tình dùng slog.Info("Request payload", "data", requestBody), và requestBody chứa Password hoặc Số thẻ tín dụng, những dữ liệu này sẽ bị phơi bày trên hệ thống quản lý log tập trung (như ELK), gây ra rủi ro bảo mật cực lớn.

Thay vì dựa vào sự cẩn thận của lập trình viên (rất dễ sai sót), cách an toàn nhất là can thiệp ở tầng thấp nhất: Viết một Custom Handler tự động che giấu (masking) các key nhạy cảm trước khi xuất ra ngoài.

1. Cơ chế thay thế giá trị (ReplaceAttr)

Rất may mắn, những người thiết kế log/slog đã lường trước bài toán này. Trong cấu trúc slog.HandlerOptions, có một hàm callback tên là ReplaceAttr.

Hàm này được slog tự động gọi cho mọi cặp Key-Value (kể cả các key mặc định như time, level, msg) ngay trước khi chúng được định dạng thành JSON hay Text. Nó cho phép bạn đánh giá Key hiện tại, và quyết định thay đổi Value của nó (hoặc bỏ luôn thuộc tính đó).

2. Thực hành: Tự động Mask các thông tin nhạy cảm

Chúng ta sẽ khởi tạo một JSONHandler kết hợp với ReplaceAttr để kiểm tra: Nếu Key có tên là password, token, hoặc credit_card, Value của chúng sẽ bị thay thế bằng chuỗi ***MASKED***.

Go

package main

import (
	"log/slog"
	"os"
	"strings"
)

// Danh sách các key cần phải che giấu (viết thường để dễ so sánh)
var sensitiveKeys = []string{"password", "token", "credit_card", "ccv", "secret"}

// Hàm kiểm tra xem một key có thuộc danh sách cấm hay không
func isSensitive(key string) bool {
	lowerKey := strings.ToLower(key)
	for _, k := range sensitiveKeys {
		// Dùng Contains để bắt các trường hợp như "user_password", "access_token"
		if strings.Contains(lowerKey, k) {
			return true
		}
	}
	return false
}

func main() {
	opts := &slog.HandlerOptions{
		// ReplaceAttr nhận vào 2 tham số:
		// - groups: Đường dẫn cấp bậc nếu đang dùng slog.Group (slice string rỗng nếu ở cấp cao nhất)
		// - a: Thuộc tính hiện tại (gồm Key và Value, kiểu slog.Attr)
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			
			// 1. Nếu là key nhạy cảm, thay thế Value bằng chuỗi che giấu
			if isSensitive(a.Key) {
				return slog.String(a.Key, "***MASKED***")
			}

			// 2. (Tùy chọn) Sửa đổi định dạng thời gian cho dễ đọc
			if a.Key == slog.TimeKey {
				t := a.Value.Time()
				// Trả về thời gian format theo chuẩn VN, hoặc bỏ qua dòng này để giữ nguyên chuẩn ISO
				return slog.String(a.Key, t.Format("2006-01-02 15:04:05"))
			}

			// Trả về nguyên bản nếu không cần can thiệp
			return a
		},
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
	slog.SetDefault(logger)

	// --- KIỂM THỬ ---

	// Trường hợp 1: Log an toàn
	slog.Info("Người dùng cập nhật email", "user_id", 123, "new_email", "admin@gmail.com")

	// Trường hợp 2: Lập trình viên vô tình log ra password (Key chính xác)
	slog.Info("Người dùng đổi mật khẩu", "user_id", 123, "password", "MyP@ssw0rd123!")

	// Trường hợp 3: Nằm trong Group, và key chứa từ khóa nhạy cảm
	slog.Info("Thanh toán đơn hàng",
		slog.Group("payment_data",
			slog.String("gateway", "Stripe"),
			slog.String("credit_card_number", "4111-2222-3333-4444"), // Sẽ bị bắt bởi strings.Contains
			slog.String("ccv", "999"),
		),
	)
}

Kết quả JSON an toàn tuyệt đối:

JSON

{"time":"2026-09-18 21:21:30","level":"INFO","msg":"Người dùng cập nhật email","user_id":123,"new_email":"admin@gmail.com"}
{"time":"2026-09-18 21:21:30","level":"INFO","msg":"Người dùng đổi mật khẩu","user_id":123,"password":"***MASKED***"}
{"time":"2026-09-18 21:21:30","level":"INFO","msg":"Thanh toán đơn hàng","payment_data":{"gateway":"Stripe","credit_card_number":"***MASKED***","ccv":"***MASKED***"}}

3. Implement Custom Handler Interface (Kỹ thuật nâng cao)

Cơ chế ReplaceAttr rất mạnh, nhưng nó có một nhược điểm: Nó chỉ chạy trên các Key-Value được truyền vào hàm log. Nếu bạn muốn tự động lấy dữ liệu từ context.Context (ví dụ lấy Request ID, Trace ID ngầm) và nhét vào log, ReplaceAttr không làm được.

Để giải quyết, bạn phải viết một struct thực thi trọn vẹn slog.Handler interface. Kỹ thuật này thường được dùng để nhúng (embed) một Handler có sẵn, và chèn thêm dữ liệu trước khi đẩy xuống Handler đó.

Go

// Đây là bộ khung chuẩn của một Custom Handler
type ContextHandler struct {
	// Nhúng một Handler bên trong (JSONHandler hoặc TextHandler)
	slog.Handler 
}

// Ghi đè hàm Handle - Hàm này được gọi mỗi khi có một dòng log phát sinh
func (h ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	// Ở đây, bạn có quyền truy cập vào 'ctx' của luồng hiện tại.
	// Bạn có thể lấy Trace ID từ ctx, sau đó dùng lệnh:
	// r.AddAttrs(slog.String("trace_id", traceID))
	
	// Sau khi nhét thêm dữ liệu, chuyển cho Handler bên trong xử lý nốt
	return h.Handler.Handle(ctx, r)
}

Kỹ thuật lấy dữ liệu từ Context chèn vào Log là xương sống của mọi hệ thống Microservices hiện đại (Distributed Tracing). Đó chính là nội dung cốt lõi của bài học tiếp theo.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí