Compare commits

..

4 Commits

Author SHA1 Message Date
AI Engineer
e8369d4680 feat(service): Client Key 应答头条件化,静态文件/WebSocket 仅 Cookie 维护,配置字段命名统一(by AI)
- Device-Id/Session-Id 仅当请求头未携带时才写入应答头
- 静态文件和 WebSocket 升级应答仅通过 Cookie 维护身份
- Client App 头改为 App-Name/App-Version(破折号命名)
- NoLogHeaders → NoLogRequestHeaders,NoLogOutputFields → NoLogResponseFields,新增 NoLogResponseHeaders
- 默认排除列表动态构建,用户只需追加自定义字段
- Cookie 头智能过滤:不再整体排除,仅剔除匹配排除列表的 key

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
2026-06-22 19:01:53 +08:00
AI Engineer
8f85c503da chore(service): 升级 id 至 v1.5.6、redis 至 v1.5.10(by AI) 2026-06-22 00:47:13 +08:00
AI Engineer
c4b8c2d3eb remove .log.meta.json 2026-06-22 00:40:26 +08:00
AI Engineer
e75cec8aaf chore(service): 升级 id 至 v1.5.5、redis 至 v1.5.9(修复 ID 未打乱问题)(by AI)
Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
2026-06-21 23:03:27 +08:00
11 changed files with 146 additions and 910 deletions

View File

@ -1,883 +0,0 @@
{
"debug": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Debug",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"discover": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "App",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 7,
"Name": "Method",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "Path",
"KeyName": "",
"AttachBefore": false,
"Color": "blue",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 9,
"Name": "Node",
"KeyName": "",
"AttachBefore": false,
"Color": "yellow",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 10,
"Name": "Attempts",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 11,
"Name": "UsedTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "%.2fms",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 12,
"Name": "Error",
"KeyName": "",
"AttachBefore": false,
"Color": "red",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 13,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"error": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Error",
"KeyName": "",
"AttachBefore": false,
"Color": "red",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "CallStacks",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"info": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Info",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"request": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "ServerId",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 7,
"Name": "App",
"KeyName": "App",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "Node",
"KeyName": "",
"AttachBefore": true,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 9,
"Name": "FromApp",
"KeyName": "From",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 10,
"Name": "FromNode",
"KeyName": "",
"AttachBefore": true,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 11,
"Name": "ClientIp",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 12,
"Name": "ClientAppName",
"KeyName": "Client",
"AttachBefore": true,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 13,
"Name": "ClientAppVersion",
"KeyName": "",
"AttachBefore": true,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 14,
"Name": "UserId",
"KeyName": "User",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 15,
"Name": "DeviceId",
"KeyName": "Device",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 16,
"Name": "SessionId",
"KeyName": "Session",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 17,
"Name": "Host",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 18,
"Name": "Method",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 19,
"Name": "Path",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 20,
"Name": "Scheme",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 21,
"Name": "Proto",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 22,
"Name": "AuthLevel",
"KeyName": "",
"AttachBefore": false,
"Color": "green",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 23,
"Name": "Priority",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 24,
"Name": "RequestData",
"KeyName": "Request",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 25,
"Name": "RequestHeaders",
"KeyName": "Headers",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 26,
"Name": "UsedTime",
"KeyName": "",
"AttachBefore": false,
"Color": "green",
"Format": "",
"Precision": 6,
"WithoutKey": false,
"Hide": false
},
{
"Index": 27,
"Name": "ResponseCode",
"KeyName": "Status",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 28,
"Name": "ResponseDataLength",
"KeyName": "ContentLength",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 29,
"Name": "ResponseData",
"KeyName": "Response",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 30,
"Name": "ResponseHeaders",
"KeyName": "Headers",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 31,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"warning": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Warning",
"KeyName": "",
"AttachBefore": false,
"Color": "yellow",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
]
}

View File

@ -1,5 +1,34 @@
# CHANGELOG - go/service
## v1.5.20 (2026-06-22)
- **Client Key 应答头条件化**:
- `Device-Id` / `Session-Id` 仅当请求头未携带时才写入应答头,避免客户端已持有 ID 时重复返回。
- 静态文件和 WebSocket 升级应答中不再写入 `Device-Id` / `Session-Id` 头,仅通过 Cookie 维护身份(浏览器 WebSocket API 不支持自定义请求头)。
- **Client App 头命名规范化**:
- 客户端上报键名从 `AppName`/`AppVersion` 改为 `App-Name`/`App-Version`(与 `Device-Id`/`Session-Id` 一致使用破折号)。
- **Breaking**: 旧版客户端需同步修改请求头名称。
- **配置字段重命名**:
- `NoLogHeaders``NoLogRequestHeaders`(请求头排除列表)。
- `NoLogOutputFields``NoLogResponseFields`(响应体字段排除)。
- 新增 `NoLogResponseHeaders`(响应头排除列表,用户可追加自定义字段)。
- **Breaking**: 使用了旧字段名的配置需同步修改。
- **动态排除列表**:
- `NoLogRequestHeaders` 默认值动态构建(包含内部标准头 + 当前配置的 client key 名),用户只需追加自己关心的额外字段。
- 移除 `init()` 中的硬编码默认值。
- **Cookie 头智能过滤**:
- `sanitizeLogHeaders``Cookie` 头不再整体排除,而是解析 Cookie 内容,仅剔除 key 命中排除列表的键值对(如 `Device-Id`/`Session-Id`),保留业务 Cookie。
- 排除列表同时适用于 Header 名匹配和 Cookie key 匹配。
- **日志应答头排除**:
- 响应头日志捕获改为使用 `effectiveNoLogResponseHeaders()`,替代之前的硬编码空字符串。
## v1.5.19 (2026-06-22)
- **依赖更新**:
- 升级依赖 `id``v1.5.6``redis``v1.5.10`
## v1.5.18 (2026-06-21)
- **依赖更新**:
- 升级依赖 `id``v1.5.5``redis``v1.5.9`(同步修复旧版本 ID 未打乱的 Bug
## v1.5.17 (2026-06-21)
- **Session 增强**:
- `Save` 改为可变参数 `Save(args ...map[string]any) error`,支持传入 map 批量设置后保存。

10
TEST.md
View File

@ -1,9 +1,9 @@
# Service Module Test Report
## 性能测试 (Benchmark)
- 测试日期: 2026-06-21
- 版本: v1.0.4
- 指标: `BenchmarkRouting`: 2791 ns/op
- 测试日期: 2026-06-22
- 版本: v1.5.20
- 指标: `BenchmarkRouting`: **5394 ns/op**
- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
## 单元测试覆盖 (Unit Test)
@ -27,7 +27,9 @@
- [x] `TestSanitizeScalars` ~ `TestSanitizeMixedSlice`: 日志脱敏 10 个测试(标量/对象/数组/嵌套/预算/Unicode
- [x] `TestSessionLogic`: Session Save/Load/Remove 及 AuthFuncs
- [x] `TestSessionInjection`: Session HTTP 注入流程
- [x] Logging Filters: NoLogInput/NoLogOutput/NoLogAllHeaders/NoLogGets/NoLogHeaders
- [x] Logging Filters: NoLogInput/NoLogOutput/NoLogAllHeaders/NoLogGets/NoLogRequestHeaders/NoLogResponseHeaders/NoLogResponseFields
- [x] Client Keys: Device-Id/Session-Id 应答头条件化(请求有则不应答)、静态文件/WebSocket 仅 Cookie
- [x] Cookie 头智能过滤: 排除列表中 key 从 Cookie 内容中剔除,保留业务 Cookie
- [x] Response Body: 200 响应和 dev 模式下 keepBody 捕获
## 基础设施对齐验证

View File

@ -26,11 +26,12 @@ type ServiceConfig struct {
NoLogInput bool // 不记录请求输入
NoLogOutput bool // 不记录响应输出
NoLogAllHeaders bool // 不记录所有请求/响应头
NoLogHeaders string // 不记录请求头中包含的这些字段,多个字段用逗号分隔
NoLogRequestHeaders string // 不记录请求头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
NoLogResponseHeaders string // 不记录响应头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
LogInputObjectNum int // 请求对象中最多记录的 key 数
LogInputArrayNum int // 请求数组中最多记录的元素数
LogInputFieldSize int // 请求单个字段的字符串截断长度
NoLogOutputFields string // 不记录响应字段中包含的这些字段
NoLogResponseFields string // 不记录响应字段中包含的这些字段
LogOutputObjectNum int // 响应对象中最多记录的 key 数
LogOutputArrayNum int // 响应数组中最多记录的元素数
LogOutputFieldSize int // 响应单个字段的字符串截断长度

4
go.mod
View File

@ -8,10 +8,10 @@ require (
apigo.cc/go/discover v1.5.3
apigo.cc/go/file v1.5.5
apigo.cc/go/http v1.5.3
apigo.cc/go/id v1.5.4
apigo.cc/go/id v1.5.6
apigo.cc/go/jsmod v1.5.3
apigo.cc/go/log v1.5.8
apigo.cc/go/redis v1.5.8
apigo.cc/go/redis v1.5.10
apigo.cc/go/safe v1.5.2
apigo.cc/go/starter v1.5.5
apigo.cc/go/timer v1.5.0

8
go.sum
View File

@ -12,16 +12,16 @@ apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ=
apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
apigo.cc/go/id v1.5.4 h1:D1Zx9gEZhOgdTgZ4SdmPImhpc9xGiOA33Y+j2MkstzQ=
apigo.cc/go/id v1.5.4/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
apigo.cc/go/id v1.5.5 h1:fQXfb2WZ4hEtzXkpb9w9o8AOcTZ44fYQfTV6iZ49l8o=
apigo.cc/go/id v1.5.5/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
apigo.cc/go/redis v1.5.8 h1:cYPA3/dzo7pHKx14BS4ZqOq1aPgWyYFewE2b0BBnLGI=
apigo.cc/go/redis v1.5.8/go.mod h1:PsBVxmoUz4aCeffvofhb0J69JriahHFWRuMU6Qkw6Pk=
apigo.cc/go/redis v1.5.9 h1:h77XPjQWcIDgxrtwjtS/gwT6PS5nSk6/x785t0OMlbY=
apigo.cc/go/redis v1.5.9/go.mod h1:YZRDLA3gWw9LAc4Z/4GrDEy8eMDMdDPDA0iqSEAu1fE=
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=

View File

@ -75,13 +75,13 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 请求头
var reqHeaders map[string]string
if !ws.Config.NoLogAllHeaders {
reqHeaders = sanitizeLogHeaders(r.Header, ws.Config.NoLogHeaders)
reqHeaders = sanitizeLogHeaders(r.Header, ws.effectiveNoLogRequestHeaders())
}
// 响应头
var respHeaders map[string]string
if !ws.Config.NoLogAllHeaders {
respHeaders = sanitizeLogHeaders(response.Header().H, "")
respHeaders = sanitizeLogHeaders(response.Header().H, ws.effectiveNoLogResponseHeaders())
}
// 请求输入脱敏
@ -434,8 +434,11 @@ func outputResult(response *Response, result any) {
func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
// SessionId
if ws.usedSessionIdKey != "" {
hasRequestSessionId := false
sessionId := request.Header().Get(ws.usedSessionIdKey)
if sessionId == "" && !ws.Config.SessionWithoutCookie {
if sessionId != "" {
hasRequestSessionId = true
} else if !ws.Config.SessionWithoutCookie {
if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil {
sessionId = ck.Value
}
@ -456,13 +459,18 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
}
}
request.Request.Header.Set(discover.HeaderSessionID, sessionId)
if !hasRequestSessionId {
response.Header().Set(ws.usedSessionIdKey, sessionId)
}
}
// DeviceId
if ws.usedDeviceIdKey != "" {
hasRequestDeviceId := false
deviceId := request.Header().Get(ws.usedDeviceIdKey)
if deviceId == "" && !ws.Config.DeviceWithoutCookie {
if deviceId != "" {
hasRequestDeviceId = true
} else if !ws.Config.DeviceWithoutCookie {
if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil {
deviceId = ck.Value
}
@ -480,16 +488,41 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
}
}
request.Request.Header.Set(discover.HeaderDeviceID, deviceId)
if !hasRequestDeviceId {
response.Header().Set(ws.usedDeviceIdKey, deviceId)
}
}
// AppName / AppVersion客户端上报注入内部标准头供下游微服务使用
// App-Name / App-Version客户端上报注入内部标准头供下游微服务使用
if ws.usedClientAppKey != "" {
if appName := request.Header().Get(ws.usedClientAppKey + "Name"); appName != "" {
if appName := request.Header().Get(ws.usedClientAppKey + "-Name"); appName != "" {
request.Request.Header.Set(discover.HeaderClientAppName, appName)
}
if appVersion := request.Header().Get(ws.usedClientAppKey + "Version"); appVersion != "" {
if appVersion := request.Header().Get(ws.usedClientAppKey + "-Version"); appVersion != "" {
request.Request.Header.Set(discover.HeaderClientAppVersion, appVersion)
}
}
}
// effectiveNoLogRequestHeaders 返回请求头排除列表(动态默认值 + 用户配置追加)
func (ws *WebServer) effectiveNoLogRequestHeaders() string {
parts := []string{"X-Request-Id", "X-Device-Id", "X-Session-Id"}
if ws.usedDeviceIdKey != "" {
parts = append(parts, ws.usedDeviceIdKey)
}
if ws.usedSessionIdKey != "" {
parts = append(parts, ws.usedSessionIdKey)
}
if ws.usedClientAppKey != "" {
parts = append(parts, ws.usedClientAppKey+"-Name", ws.usedClientAppKey+"-Version")
}
if ws.Config.NoLogRequestHeaders != "" {
parts = append(parts, ws.Config.NoLogRequestHeaders)
}
return strings.Join(parts, ",")
}
// effectiveNoLogResponseHeaders 返回响应头排除列表(用户配置追加)
func (ws *WebServer) effectiveNoLogResponseHeaders() string {
return ws.Config.NoLogResponseHeaders
}

View File

@ -137,14 +137,21 @@ func sanitizeSliceContent(s []any, opts sanitizeOpts, budget *int) []any {
return result
}
// sanitizeLogHeaders 过滤请求/响应头,排除 NoLogHeaders 中指定的字段
// sanitizeLogHeaders 过滤请求/响应头,排除 noLogHeaders 中指定的字段。
// 对于 Cookie 头,不整体排除,而是从 Cookie 内容中剔除 key 命中排除列表的键值对。
func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
result := make(map[string]string)
excludes := strings.Split(noLogHeaders, ",")
excludes := splitAndTrim(noLogHeaders)
for k, v := range h {
if k == "Cookie" && len(excludes) > 0 {
if filtered := sanitizeCookieValue(v, excludes); filtered != "" {
result[k] = filtered
}
continue
}
skip := false
for _, ex := range excludes {
if ex != "" && strings.EqualFold(k, strings.TrimSpace(ex)) {
if strings.EqualFold(k, ex) {
skip = true
break
}
@ -156,14 +163,52 @@ func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
return result
}
// sanitizeCookieValue 从 Cookie 原始值中剔除 key 命中排除列表的键值对。
// 输入为原始 Cookie 头值(可能多个 key=value 以 "; " 或 ";" 分隔)。
func sanitizeCookieValue(values []string, excludes []string) string {
raw := strings.Join(values, "; ")
pairs := strings.Split(raw, ";")
var kept []string
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
key, _, _ := strings.Cut(pair, "=")
key = strings.TrimSpace(key)
excluded := false
for _, ex := range excludes {
if strings.EqualFold(key, ex) {
excluded = true
break
}
}
if !excluded {
kept = append(kept, pair)
}
}
return strings.Join(kept, "; ")
}
func splitAndTrim(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
return parts
}
// sanitizeRespBody 对响应体进行脱敏:尝试 JSON 解析后走对象脱敏,失败则按字符串截断
func sanitizeRespBody(body []byte, cfg *ServiceConfig) any {
// 尝试解析为 JSON 对象
var parsed any
if err := cast.UnmarshalJSON(body, &parsed); err == nil && parsed != nil {
// 排除敏感字段
if cfg.NoLogOutputFields != "" {
parsed = stripFields(parsed, cfg.NoLogOutputFields)
if cfg.NoLogResponseFields != "" {
parsed = stripFields(parsed, cfg.NoLogResponseFields)
}
return sanitizeLogData(parsed, sanitizeOpts{
maxSize: 200,

View File

@ -111,7 +111,7 @@ var DefaultServer = NewWebServer()
var Config = &DefaultServer.Config
func init() {
Config.NoLogHeaders = "X-Request-Id,X-Device-Id,X-Session-Id,Cookie,Device-Id,Session-Id"
// NoLogRequestHeaders / NoLogResponseHeaders 的默认值在日志捕获时动态构建
Config.LogInputObjectNum = 10
Config.LogInputArrayNum = 5
Config.LogInputFieldSize = 20

View File

@ -131,6 +131,10 @@ func (ws *WebServer) processStatic(requestPath string, request *Request, respons
return false
}
// 静态文件通过 Cookie 维护 ID不应答 Device-Id / Session-Id 头
response.Header().Del(ws.usedDeviceIdKey)
response.Header().Del(ws.usedSessionIdKey)
// 检查 304
if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {

View File

@ -78,6 +78,11 @@ func Upgrade(response *Response, request *Request) (*WebSocketConn, error) {
}
func (ws *WebServer) doWebsocketService(wsc *websocketServiceType, request *Request, response *Response, logger *log.Logger, object any) {
// WebSocket 浏览器 API 不支持自定义请求头,只能通过 Cookie 传递身份标识,
// 因此不应在升级应答中返回 Device-Id / Session-Id 头。
response.Header().Del(ws.usedDeviceIdKey)
response.Header().Del(ws.usedSessionIdKey)
wsConn, err := Upgrade(response, request)
if err != nil {
logger.Error("websocket upgrade failed", "error", err.Error())