功能说明
分布式系统中一次用户操作可能经过网关、多个服务和队列,日志没有统一标识就难以串联。请求 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)
}