6050d11f27
- 创建基于 CloudWego Hertz 的 Go 微服务脚手架 - 集成 Nacos 服务注册/发现功能 - 添加 gRPC 客户端支持 - 实现环境变量配置管理 (.env.example) - 添加 HTTP 中间件 (Recovery, AccessLog, CORS) - 配置 Gitea CI/CD 构建部署流程 BREAKING CHANGE: 项目结构调整,从简单的 API 服务升级为完整的微服务架构
133 lines
3.0 KiB
Go
133 lines
3.0 KiB
Go
package httplog
|
||
|
||
import (
|
||
"encoding/json"
|
||
"mime"
|
||
"strings"
|
||
|
||
"github.com/cloudwego/hertz/pkg/app"
|
||
)
|
||
|
||
var sensitiveKeys = []string{"password", "secret", "token", "key", "passwd"}
|
||
|
||
const redactedValue = "[REDACTED]"
|
||
|
||
// desensitization:只对第一层 key 做脱敏,不递归
|
||
func desensitization(data map[string]any) ([]byte, error) {
|
||
out := make(map[string]any, len(data))
|
||
|
||
for k, v := range data {
|
||
if isSensitiveKey(k) {
|
||
out[k] = redactedValue
|
||
} else {
|
||
out[k] = v
|
||
}
|
||
}
|
||
return json.Marshal(out)
|
||
}
|
||
|
||
// 大小写不敏感、包含式匹配
|
||
func isSensitiveKey(key string) bool {
|
||
k := strings.ToLower(key)
|
||
for _, s := range sensitiveKeys {
|
||
if strings.Contains(k, s) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// FormBodyToJSONWithFilePlaceholder :
|
||
// - 普通表单:完整转 JSON
|
||
// - multipart 文件字段:不取任何文件信息,只输出 "[file]" 或 ["[file]", ...]
|
||
func FormBodyToJSONWithFilePlaceholder(c *app.RequestContext) (jsonBytes []byte, ok bool, err error) {
|
||
ct := string(c.Request.Header.ContentType())
|
||
mediaType, _, _ := mime.ParseMediaType(ct)
|
||
mediaType = strings.ToLower(mediaType)
|
||
|
||
out := make(map[string]any)
|
||
|
||
switch mediaType {
|
||
case "application/x-www-form-urlencoded":
|
||
args := c.PostArgs()
|
||
args.VisitAll(func(k, v []byte) {
|
||
key := string(k)
|
||
// 同名 key 多次出现时,转成数组
|
||
if old, exists := out[key]; exists {
|
||
switch x := old.(type) {
|
||
case string:
|
||
out[key] = []string{x, string(v)}
|
||
case []string:
|
||
out[key] = append(x, string(v))
|
||
default:
|
||
out[key] = string(v)
|
||
}
|
||
} else {
|
||
out[key] = string(v)
|
||
}
|
||
})
|
||
b, e := desensitization(out)
|
||
return b, true, e
|
||
|
||
case "multipart/form-data":
|
||
form, e := c.MultipartForm()
|
||
if e != nil {
|
||
return nil, false, e
|
||
}
|
||
|
||
// 普通字段
|
||
for k, vv := range form.Value {
|
||
if len(vv) == 1 {
|
||
out[k] = vv[0]
|
||
} else {
|
||
out[k] = vv
|
||
}
|
||
}
|
||
|
||
// 文件字段:只做占位,不读取任何文件信息
|
||
for k, files := range form.File {
|
||
if len(files) <= 1 {
|
||
out[k] = "[file]"
|
||
} else {
|
||
arr := make([]string, 0, len(files))
|
||
for range files {
|
||
arr = append(arr, "[file]")
|
||
}
|
||
out[k] = arr
|
||
}
|
||
}
|
||
|
||
b, e2 := desensitization(out)
|
||
return b, true, e2
|
||
|
||
default:
|
||
// 不是表单就不处理
|
||
return nil, false, nil
|
||
}
|
||
}
|
||
|
||
// ResponseBodySnippet :获取响应内容
|
||
func ResponseBodySnippet(c *app.RequestContext, maxBytes int) (body string, ok bool, truncated bool) {
|
||
b := c.Response.Body()
|
||
if len(b) == 0 {
|
||
return "", false, false
|
||
}
|
||
|
||
// 只采集“看起来像文本/JSON”的响应,其他(图片/zip等)直接跳过
|
||
ct := strings.ToLower(string(c.Response.Header.ContentType()))
|
||
if ct != "" && !(strings.HasPrefix(ct, "text/") ||
|
||
strings.Contains(ct, "application/json") ||
|
||
strings.Contains(ct, "application/xml") ||
|
||
strings.Contains(ct, "application/javascript")) {
|
||
return "", false, false
|
||
}
|
||
|
||
if maxBytes > 0 && len(b) > maxBytes {
|
||
b = b[:maxBytes]
|
||
truncated = true
|
||
}
|
||
|
||
body = string(b)
|
||
return body, true, truncated
|
||
}
|