Files
shiran 6050d11f27 feat: 添加微服务模板基础架构
- 创建基于 CloudWego Hertz 的 Go 微服务脚手架
- 集成 Nacos 服务注册/发现功能
- 添加 gRPC 客户端支持
- 实现环境变量配置管理 (.env.example)
- 添加 HTTP 中间件 (Recovery, AccessLog, CORS)
- 配置 Gitea CI/CD 构建部署流程

BREAKING CHANGE: 项目结构调整,从简单的 API 服务升级为完整的微服务架构
2026-04-15 11:13:38 +08:00

133 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}