feat(log): 前后端共用 request_id 与四时间点时钟校准

- X-Request-Id 请求头带合法 UUID 则沿用为 request_id(严格格式白名单防注入),否则自生成 12 位 hex
- 响应头新增 X-Server-Received-Ms / X-Server-Sent-Ms 供前端时钟校准
- CORS Allow-Headers 增加 X-Request-Id,Expose-Headers 显式列出(credentials 下不认 *)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-27 07:46:38 +08:00
parent e22df36d3f
commit d3e71decf4
4 changed files with 157 additions and 9 deletions
+39 -6
View File
@@ -208,6 +208,30 @@ func truncateUA(s string, max int) string {
return string(r[:max])
}
// normalizeRequestId 校验前端传入的 X-Request-Id。
// 仅接受标准 UUID 形态(36 位,8-4-4-4-12,hex+连字符),统一转小写;
// 其余(超长、控制符、任意注入串)一律丢弃返回空,保证日志字段安全且低基数可控。
func normalizeRequestId(v string) string {
if len(v) != 36 {
return ""
}
for i := 0; i < 36; i++ {
c := v[i]
switch i {
case 8, 13, 18, 23:
if c != '-' {
return ""
}
default:
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
if !isHex {
return ""
}
}
}
return strings.ToLower(v)
}
// shortID 生成 n 字节随机 hexn=6 → 12 位)
func shortID(n int) string {
b := make([]byte, n)
@@ -557,13 +581,20 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
unescapeUrl = req.RequestURI
}
// 请求级追踪:sid=sessionId 前 12 位(脱敏),request_id 回写响应头
requestId := shortID(6)
// 请求级追踪:sid=sessionId 前 12 位(脱敏),request_id 回写响应头
// 前后端共用一个 request_id:前端带合法 X-Request-Id(UUID)则沿用,非法/缺失才服务端自生成,
// 严格格式校验杜绝日志注入。
requestId := normalizeRequestId(req.Header.Get("X-Request-Id"))
if requestId == "" {
requestId = shortID(6)
}
sid := sessionId
if len(sid) > 12 {
sid = sid[:12]
}
w.Header().Set("X-Request-Id", requestId)
// 服务端接收时间(ms),与 View() 写出的 X-Server-Sent-Ms 组成四时间点时钟校准
w.Header().Set("X-Server-Received-Ms", strconv.FormatInt(nowUnixTime.UnixMilli(), 10))
reqLog := that.Log.WithFields("sid", sid, "request_id", requestId)
dbCopy := that.Db
dbCopy.Log = reqLog
@@ -716,8 +747,9 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
//header.Set("Access-Control-Allow-Origin", "*")
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
// credentials 模式下浏览器不认 "*",必须显式列出可读响应头
header.Set("Access-Control-Expose-Headers", "X-Request-Id,X-Server-Received-Ms,X-Server-Sent-Ms")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie,X-Request-Id")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果
@@ -763,8 +795,9 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
// credentials 模式下浏览器不认 "*",必须显式列出可读响应头
header.Set("Access-Control-Expose-Headers", "X-Request-Id,X-Server-Received-Ms,X-Server-Sent-Ms")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie,X-Request-Id")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果