应优先从Authorization头提取API密钥(支持“ApiKey xxx”格式),未命中再查api_key查询参数;使用crypto/subtle.ConstantTimeCompare安全比对,长度不等直接拒绝;密钥须外部注入、禁止硬编码与日志明文输出。

怎么从Header或Query里取API密钥
Go的http.Request本身不区分认证来源,得自己写逻辑判断——先看Authorization头,再 fallback 到api_key查询参数,别指望框架自动合并。顺序错了,攻击者可能绕过Header校验直接用Query注入。
- 优先检查
r.Header.Get("Authorization"),按惯例支持ApiKey xxx格式(注意空格分隔) - 没命中再查
r.URL.Query().Get("api_key"),别用r.FormValue——它会触发ParseForm,对非POST请求可能读空body或panic - 两个都为空就直接
http.Error(r, "Unauthorized", http.StatusUnauthorized),别留空字符串进去校验
怎么安全比对API密钥
用crypto/subtle.ConstantTimeCompare,不是==。密钥通常存在环境变量或DB里,明文比较会被时序攻击利用——差一个字节就快几纳秒,攒够1000次请求就能暴力推断出正确值。
- 先把存储的密钥转成
[]byte,再把请求里的密钥也转[]byte,长度不等直接拒绝(避免ConstantTimeCompare输入长度差异暴露信息) - 别把密钥拼进日志:
log.Printf("auth failed for key: %s", key)这种写法等于公开密钥 - 如果密钥存在Redis里,用
GET而不是SCAN——后者可能漏掉过期key,导致“已删密钥仍能用”
怎么让中间件不影响其他Handler逻辑
Go的HTTP中间件本质是函数套函数,http.HandlerFunc签名固定,但你不能在中间件里提前return后还让后续Handler执行——必须显式终止调用链。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 典型错误:中间件里写了
if !valid { http.Error(...) }却没return,后面Handler照常运行,造成鉴权失效 - 正确写法是
if !valid { http.Error(...); return },或者用http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... })包一层确保控制流清晰 - 如果用了
gorilla/mux这类路由库,别把鉴权逻辑塞进Router.HandleFunc闭包里——每次请求都新建函数,GC压力大;应该复用一个预定义的中间件函数
为什么不要把API密钥写死在代码里
哪怕只是临时测试,const apiKey = "xxx"也会被git history记住,CI日志可能打印出来,Docker镜像层里也能strings搜到。生产环境密钥必须外部注入。
立即学习“go语言免费学习笔记(深入)”;
- 用
os.Getenv("API_KEY")读取,启动时通过docker run -e API_KEY=...或K8s Secret挂载 - 如果密钥需要轮换,硬编码会导致必须发版——而环境变量+重启服务就能切换
- 本地开发用
.env文件?小心go build时没加载,建议用github.com/joho/godotenv在main()开头显式加载,且只在!os.Getenv("ENVIRONMENT") == "production"时启用
密钥校验本身很简单,难的是边界情况:Header大小写、Query编码、并发读写缓存、日志脱敏、密钥轮换时的双写窗口——这些地方一漏,前面写的逻辑全白搭。

















