如何正确实现仅面向第三方的API Key鉴权机制
现有实现的核心问题
你当前写的中间件存在3个直接影响逻辑正确性的bug,以及方案选型的缺陷:
- 白名单命中后未终止逻辑:匹配到内网IP后调用
next.ServeHTTP没有加return,代码会继续往下执行API Key校验逻辑,白名单规则完全不生效 - 数据库查询参数错误:
sqlx.Get要求第二个参数传入结构体指针才能写入查询结果,你当前传的是结构体值,所有携带合法API Key的请求都会查询失败被拦截 - IP白名单方案可靠性差:如果服务前挂了Nginx/CDN等反向代理,
r.RemoteAddr拿到的是代理节点IP而非真实客户端IP,白名单极易误判;如果主站多实例部署IP变动,需要频繁修改硬编码配置,维护成本极高
主站免鉴权的落地方案
按可靠性和易用性排序,推荐以下两种方案,不要纯靠IP白名单做内部放行:
- 优先选端口隔离方案:给服务同时开两个监听端口,一个端口绑定公网网卡,挂载当前的API鉴权中间件,对外提供第三方调用服务;另一个端口只绑定内网网卡,不挂载鉴权中间件,主站所有调用走内网端口,访问权限由服务器安全组/防火墙控制,只有内网来源能访问,逻辑零侵入,性能最高,没有伪造风险
- 次选内部标识方案:如果没法做端口隔离,就在主站发起请求时加自定义请求头
X-Internal-Auth,值是提前配置好的高复杂度随机密钥,中间件优先校验这个头:如果值和服务端配置的内部密钥一致,直接放行跳过后续鉴权。注意要在最外层网关层配置规则,剥离外部请求携带的X-Internal-Auth头,避免外部用户伪造内部请求。
注意:无论用哪种放行规则,命中后一定要加return终止当前中间件逻辑,不要继续往下执行鉴权流程
限流计数的存储选型
根据部署架构和性能要求选即可,没有绝对的最优解:
- 单实例部署、允许服务重启后限流计数重置的场景:直接存在服务内存即可,用标准的滑动窗口/令牌桶算法实现,没有额外IO开销,性能最高
- 多实例部署、要求计数全局准确的场景:不要直接存在关系型数据库,高并发下频繁更新计数会给数据库造成极大压力,很容易成为性能瓶颈。优先用Redis做计数存储,基于
INCR+EXPIRE指令实现窗口限流,性能足够,多实例之间计数共享,是工业界通用的实现方案 - 只有当单Key限流阈值极低(比如单日调用次数<100次)、整体QPS极小的场景,才考虑把计数持久化到关系型数据库
修正后的中间件参考代码
// 提前在配置文件中定义内部请求密钥、白名单列表等配置,不要硬编码 func APIRequestMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 优先校验内部请求标识,命中直接放行 internalAuth := r.Header.Get("X-Internal-Auth") if internalAuth == config.Conf.InternalRequestSecret { next.ServeHTTP(w, r) return } // 若要保留IP白名单能力,注意正确处理反向代理场景的真实IP // 代理场景下要从X-Forwarded-For链取第一个可信IP,不要直接用RemoteAddr // hostIP, _, err := net.SplitHostPort(r.RemoteAddr) // if err != nil { // logerr.FromError(err) // hostIP = r.RemoteAddr // } // if string_util.StringInArray(&hostIP, config.Conf.AllowedInternalIPs) { // next.ServeHTTP(w, r) // return // } auth := r.Header.Get("authorization") type tokenDetails struct { Token string `db:"token"` Expires_at time.Time `db:"expires_at"` Rate_limit int `db:"rate_limit"` Enabled bool `db:"enabled"` Created time.Time `db:"created"` } var details tokenDetails // 传入结构体指针,保证sqlx能正确写入查询结果 err := sqlx.Get(database.AccountDB, &details, `SELECT * FROM keys WHERE token = $1`, auth) if err != nil { request_util.InvalidTokenResponse(w, r) return } if details.Expires_at.Before(time.Now()) { request_util.InvalidTokenResponse(w, r) return } if !details.Enabled { request_util.InvalidTokenResponse(w, r) return } // 多实例场景推荐用Redis实现限流计数 // rateKey := fmt.Sprintf("api_limit:%s", details.Token) // currentCount, _ := redis.Client.Incr(r.Context(), rateKey).Result() // if currentCount == 1 { // redis.Client.Expire(r.Context(), rateKey, time.Minute) // } // if currentCount > int64(details.Rate_limit) { // request_util.RateLimitResponse(w, r) // return // } next.ServeHTTP(w, r) }) }
内容的提问来源于stack exchange,提问作者AlicSt
相关产品推荐
相关产品推荐

