You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确实现仅面向第三方的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:54:17