API安全需分层保障身份可信、请求未篡改、调用不越权、流量不压垮四点;JWT校验须自定义宽松时间容错(±30秒),避免时钟不同步导致误拒。
直接说结论:api 安全不是加个 jwt 或开个 https 就完事,而是要分层卡住「身份可信」「请求未篡改」「调用不越权」「流量不压垮」四个关键点。漏掉任一环,都可能被绕过。
JWT 中间件必须自定义时间校验和 kid 路由
用 github.com/golang-jwt/jwt/v5 时,默认 ParseWithClaims 会严格校验 exp 和 nbf,但生产环境服务器与客户端时钟不同步很常见——差 2 秒就拒签,用户直接报错 401。
必须显式传入宽松校验逻辑:
- 用
jwt.WithValidator替换默认校验器,允许 ±30 秒偏差(abs(now.UnixMilli() - exp) ) - 密钥轮换时,
keyFunc不能只返回一个公钥;要先从token.Header["kid"]取值,再查 map 返回对应密钥 - 如果 header 没
kid,keyFunc必须返回nil, errors.New("missing kid"),不能 panic,否则 recovery 中间件会失效
签名验签中间件要防 body 重复读 + 路径/参数归一化
签名原文拼接错一点,服务端重算的签名就跟客户端对不上。常见坑是直接用 r.URL.Path 或 r.FormValue() 构造原文——它们已被自动 decode 或排序打乱。
正确做法是:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 路径用
r.URL.EscapedPath()(保留原始编码,如%2F不变) - query 参数从
r.URL.RawQuery解析,再按字典序排序键名、URL decode 值后拼接 - body 只能读一次:用
io.TeeReader(r.Body, buf)或提前io.ReadAll到bytes.Buffer,再分别供验签和业务 handler 使用 - 验签前必须调用
r.Body = io.NopCloser(buf),否则后续 handler 读不到 body
Logger 和 Recovery 中间件配置不当会反向泄露敏感信息
很多团队开了 fiberRecover.Config.EnableStackTrace = true 就以为“有堆栈了”,但没关响应写入,panic 信息会直接混进 JSON 响应体里返回给客户端。
两个关键动作缺一不可:
-
Logger中间件必须补全RequestID、User-Agent、BodyHash(SHA256(body)),否则审计时无法定位异常请求源头;补BodyHash前得先调ctx.Request().DisableBodyConsumption(),否则业务 handler 读不到 body -
Recovery的StackTraceHandler函数里,只能调l.Error()记日志,绝不能调w.Write()或ctx.Send();且要在 handler 结束后显式执行ctx.Response().Reset()清空已写入的响应缓冲区
敏感配置必须 runtime 校验 + 构建期隔离
硬编码密钥、用 os.Getenv("SECRET") || "dev-key" 当默认值、在日志里打完整 config 结构体——这三类操作在上线当天就可能把密钥送进 Sentry 或 Kibana。
真正落地要卡死三处:
- 所有密钥字段(JWT secret、HMAC key、DB password)必须用
os.LookupEnv显式判断存在性与非空,缺失立即log.Fatal,不兜底 - 本地开发用
godotenv.Load(),但只允许在main包调用;.env必须gitignore,CI 环境禁用该文件,改用平台原生 secrets 注入 - 结构体定义敏感字段时加
json:"-" yaml:"-",并实现String()方法返回脱敏形式(如SecretKey: "***"),避免fmt.Printf("%+v")泄露
最常被忽略的是中间件顺序:logger 必须在 recovery 之前,否则 panic 日志没 requestid;签名验签必须在 jwt 验证之前,否则未验签就放行 token 解析,攻击者可伪造任意 claim;这些顺序一旦写错,安全机制形同虚设。

















