← 中文资源

简体中文版本

请求 ID 透传中间件

校验或生成请求 ID,写入响应头并通过私有上下文键暴露给日志与下游调用,便于跨服务串联。

阅读英文原文

功能说明

分布式系统中一次用户操作可能经过网关、多个服务和队列,日志没有统一标识就难以串联。请求 ID 中间件在入口读取上游传来的 X-Request-ID;如果缺失或格式非法,就生成新的随机值。该值会写回响应头,方便客户端报错时提供,同时放进请求上下文,业务处理器可将其写入日志或继续传给下游。

直接信任客户端传入的请求头有风险,攻击者可以插入换行、控制字符或超长字符串污染日志。validRequestID 把允许字符限制为字母、数字、连字符和下划线,长度限制在一百二十八以内。main 注册一个演示处理器,打印当前请求的 ID。

关键代码

validRequestID 用 range 遍历 Unicode 码点,非 ASCII 字符落入默认分支被拒绝,避免同形字符和不可见字符。RequestID 中间件先取请求头,校验失败时用 crypto/rand 生成八字节并转为十六进制,绝不使用可预测的计数器。响应头在调用业务前设置,业务即使返回错误也能带上 ID。context.WithValue 使用未导出的空结构体键,避免与其他包冲突;RequestIDFrom 做类型断言,不存在时返回空字符串。

使用注意

请求 ID 只是关联标识,不是认证凭据,不能用于授权。日志中应统一字段名并限制长度,防止日志注入。若上游已经生成 ID,应明确信任边界,公网入口必须重新校验,内部服务可以按策略透传。异步任务需要把 ID 显式放入消息体或任务上下文,context 不会自动跨进程传播。高并发下 crypto/rand 开销可接受,但不要在每个日志行重复读取随机源。

验证方式

启动服务后请求 /,响应头包含 X-Request-ID,正文打印相同值。再次请求并手动带上合法的 X-Request-ID: trace-123,正文应原样返回该值。带上包含空格或换行的非法值,响应头会变成新的十六进制 ID,说明输入未被直接信任。

代码示例

package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
)

type requestIDKey struct{}

func validRequestID(id string) bool {
	if len(id) == 0 || len(id) > 128 {
		return false
	}
	for _, char := range id {
		switch {
		case char >= 'a' && char <= 'z':
		case char >= 'A' && char <= 'Z':
		case char >= '0' && char <= '9':
		case char == '-' || char == '_':
		default:
			return false
		}
	}
	return true
}

func RequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if !validRequestID(id) {
			buffer := make([]byte, 8)
			if _, err := rand.Read(buffer); err != nil {
				http.Error(w, "cannot create request id", http.StatusInternalServerError)
				return
			}
			id = hex.EncodeToString(buffer)
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), requestIDKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func RequestIDFrom(ctx context.Context) string {
	id, _ := ctx.Value(requestIDKey{}).(string)
	return id
}

func main() {
	handler := RequestIDMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "request", RequestIDFrom(r.Context()))
	}))
	http.Handle("/", handler)
	_ = http.ListenAndServe(":8080", nil)
}