如何在 Nginx Lua 脚本中实现 HMAC 签名验签 API 鉴权?

文章导读
在 Nginx 中实现 HMAC 验签,最稳妥的方案是基于 OpenResty 搭配 lua-resty-string 库,在 access_by_lua 阶段完成计算与比对。这适合需要高性能网关鉴权的场景。
📋 目录
  1. 完整配置示例
  2. 客户端签名生成脚本
  3. 验证与排查
  4. 常见坑
  5. 参考来源
A A

在 Nginx 中实现 HMAC 验签,最稳妥的方案是基于 OpenResty 搭配 lua-resty-string 库,在 access_by_lua 阶段完成计算与比对。这适合需要高性能网关鉴权的场景。

先说结论:使用 OpenResty 的 lua-resty-string 模块处理加密运算,配合 access_by_lua 拦截请求,是兼顾性能与维护性的做法。

如何在 Nginx Lua 脚本中实现 HMAC 签名验签 API 鉴权?
  • 先判断:确认 Nginx 已编译 lua 模块且可加载 resty 库
  • 优先做:在 Lua 脚本中统一构建签名字符串,避免前后端规则不一致
  • 再验证:通过 curl 携带签名头请求,观察日志与状态码

完整配置示例

以下配置展示了如何在 http 块加载模块,并在 location 中集成验签逻辑。注意密钥不要硬编码,生产环境建议使用环境变量或配置中心。

http {
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    server {
        location /api/ {
            access_by_lua_block {
                -- 引入依赖
                local hmac = require "resty.hmac"
                local str = require "resty.string"
                local ngx_var = ngx.var

                -- 1. 获取请求头参数,防止 nil 报错
                local timestamp = ngx_var.http_x_timestamp
                local client_sign = ngx_var.http_x_signature
                if not timestamp or not client_sign then
                    ngx.log(ngx.ERR, "missing timestamp or signature")
                    return ngx.exit(403)
                end

                -- 2. 检查时间戳过期 (假设允许 5 分钟误差)
                local now = ngx.time()
                if math.abs(now - tonumber(timestamp)) > 300 then
                    ngx.log(ngx.ERR, "timestamp expired")
                    return ngx.exit(403)
                end

                -- 3. 构建待签名字符串 (必须与客户端一致)
                local str_to_sign = ngx_var.request_method .. ":" .. ngx_var.uri .. ":" .. timestamp
                local secret = "your_secret_key" -- 生产环境请替换为动态获取

                -- 4. 计算 HMAC SHA256
                local hm = hmac:new(secret, hmac.ALG_SHA256)
                hm:update(str_to_sign)
                local sign_binary = hm:final()

                -- 5. 转为 hex 并比对 (关键步骤:binary 转 hex)
                local server_sign = str.to_hex(sign_binary)
                if server_sign ~= client_sign then
                    ngx.log(ngx.ERR, "signature mismatch")
                    return ngx.exit(403)
                end
            }
            proxy_pass http://backend;
        }
    }
}

客户端签名生成脚本

为了验证 Nginx 配置,可以使用以下 Python 脚本生成合法的签名头。确保待签名字符串拼接规则与 Nginx 端完全一致。

如何在 Nginx Lua 脚本中实现 HMAC 签名验签 API 鉴权?
import hmac
import hashlib
import time

secret = "your_secret_key"
timestamp = str(int(time.time()))
method = "GET"
uri = "/api/test"

# 构建待签名字符串
str_to_sign = f"{method}:{uri}:{timestamp}"

# 计算 HMAC SHA256
signature = hmac.new(secret.encode(), str_to_sign.encode(), hashlib.sha256).hexdigest()

print(f"X-Timestamp: {timestamp}")
print(f"X-Signature: {signature}")
# 使用示例:curl -H "X-Timestamp: {timestamp}" -H "X-Signature: {signature}" http://your-domain.com/api/test

验证与排查

使用 curl 命令模拟携带签名的请求,并观察 Nginx 错误日志。

如何在 Nginx Lua 脚本中实现 HMAC 签名验签 API 鉴权?
curl -H "X-Timestamp: 1700000000" -H "X-Signature: 计算出的 hex 值" http://your-domain.com/api/test

观察 Nginx 错误日志(通常位于 /usr/local/openresty/nginx/logs/error.log)。如果验签失败,应看到自定义的日志记录;如果验签成功,请求应正常返回 200 状态码。也可以故意篡改签名值,确认是否返回 403。

常见坑

  • 时间同步:客户端与服务端时间偏差过大会导致验签失败。通常允许±5 分钟误差,需在 Lua 脚本中做时间差判断。
  • 字符串构建规则:前后端构建“待签名字符串”的顺序必须完全一致(例如参数是否排序、是否包含查询字符串)。一致性是生效的前提。
  • 密钥安全:密钥不能明文写在代码库里。如果 Nginx 配置被泄露,密钥也会泄露。建议结合 IP 白名单或定期轮换密钥。
  • HTTPS 强制:签名本身不加密传输内容。如果通过 HTTP 传输,签名和密钥可能被中间人截获。务必在 Nginx 层强制跳转 HTTPS。

参考来源

  • OpenResty 官方文档,页面标题:OpenResty® - Official Site,URL:https://openresty.org/
  • lua-resty-string 项目仓库,页面标题:openresty/lua-resty-string,URL:https://github.com/openresty/lua-resty-string