Nginx在ssl_client_certificate+ssl_verify_client on阶段仅校验签名有效性、有效期及信任链,不检查CN/SAN或业务策略;细粒度校验需在access_by_lua*阶段用Lua解析$ssl_client_s_dn等变量或resty.openssl进行。

Nginx 本身在 ssl_client_certificate + ssl_verify_client on 阶段只做基础 TLS 层校验:验证客户端证书是否由指定 CA 签发、签名是否有效、是否在有效期内。它不检查 CN/SAN、不解析扩展字段、也不执行业务级策略判断(比如“只允许 CN=app-ios 的证书接入”)。这类更细粒度的合法性校验,必须交由 Lua 在请求处理阶段完成。
要实现在客户端验证阶段用 Lua 校验证书合法性,核心是利用 Nginx 提供的 SSL 变量,并在 access_by_lua* 阶段读取、解析、判断。
✅ 前提条件:TLS 双向认证已启用
确保 Nginx 已配置基础 mTLS,否则 Lua 拿不到客户端证书信息:
ssl_client_certificate /etc/nginx/certs/ca-bundle.pem; # PEM 格式可信 CA(含根或中间) ssl_verify_client on; ssl_verify_depth 2;
只有当 ssl_verify_client on 且客户端成功提交了被信任链签发的证书时,Nginx 才会填充 $ssl_client_cert、$ssl_client_s_dn、$ssl_client_i_dn 等变量。
✅ 在 access 阶段用 Lua 读取并校验证书字段
Nginx 不直接暴露证书结构,但提供几个关键变量供 Lua 使用:
-
$ssl_client_cert:Base64 编码的 PEM 格式客户端证书(含-----BEGIN CERTIFICATE-----头尾) -
$ssl_client_s_dn:证书 Subject DN 字符串,例如CN=client1,OU=dev,O=example -
$ssl_client_i_dn:Issuer DN 字符串 -
$ssl_client_serial:十六进制序列号(如0123456789ABCDEF) -
$ssl_client_v_end:证书过期时间(ISO8601 格式,如20260930120000Z)
⚠️ 注意:
$ssl_client_cert是完整 PEM,需先清理换行和头尾标记,再 Base64 解码才能解析 ASN.1 结构;但多数业务校验只需 DN 或序列号,可直接用正则提取。
示例:Lua 脚本校验 CN 是否为白名单值
-- /usr/local/nginx/conf/lua/check-client-cert.lua
local dn = ngx.var.ssl_client_s_dn
if not dn then
ngx.status = 401
ngx.say("Missing client certificate")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 提取 CN 值(支持 CN=xxx 或 CN = xxx)
local cn_match = string.match(dn, "CN%s*=%s*([^,]+)")
if not cn_match then
ngx.log(ngx.ERR, "Failed to extract CN from DN: ", dn)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 白名单校验
local valid_cns = { "webapp", "mobile-ios", "backend-svc" }
local is_allowed = false
for _, v in ipairs(valid_cns) do
if cn_match == v then
is_allowed = true
break
end
end
if not is_allowed then
ngx.log(ngx.WARN, "Rejected client CN: ", cn_match)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end在 location 中调用:
location /api/ {
ssl_verify_client on;
proxy_pass http://backend;
access_by_lua_file /usr/local/nginx/conf/lua/check-client-cert.lua;
}✅ 进阶:校验证书扩展字段(如 SAN、Key Usage)
若需检查 Subject Alternative Name(SAN)或 Extended Key Usage(EKU),仅靠 $ssl_client_s_dn 不够,必须解析证书内容。
推荐做法(无需外部依赖):
- 使用
openssl x509 -in <cert> -text -noout命令解析(需os.execute,不推荐生产环境) - 更安全可靠的方式:用 Lua 的
resty.openssl(OpenResty 1.21.4+ 内置)或luaossl解析 PEM
简单示例(使用 resty.openssl.x509):
local x509 = require "resty.openssl.x509"
local pem = ngx.var.ssl_client_cert
local cert, err = x509.new(pem)
if not cert then
ngx.log(ngx.ERR, "Failed to parse cert: ", err)
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 检查 EKU 是否含 clientAuth
local eku = cert:get_extension_object("extendedKeyUsage")
if eku and not string.find(eku, "clientAuth") then
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 检查 SAN 中是否有指定域名
local san = cert:get_subject_alt_name()
if san and type(san) == "table" then
for _, item in ipairs(san) do
if item.type == "DNS" and item.value == "api.example.com" then
return
end
end
end
ngx.exit(ngx.HTTP_UNAUTHORIZED)✅ 此方式要求 OpenResty ≥ 1.21.4.2 且编译时启用了 OpenSSL 3 支持。
❌ 常见误区提醒
-
proxy_ssl_verify和ssl_client_certificate完全无关:前者是 Nginx 验证后端 HTTPS 服务,后者才是验证客户端。 - 不要试图在
init_by_lua或set_by_lua中读取$ssl_client_*变量——它们只在 SSL 握手完成后、access阶段才可用。 -
$ssl_client_cert默认是空字符串(未提供证书)或完整 PEM;若ssl_verify_client optional,需先判空再解析。 - 不建议在 Lua 中做耗时操作(如远程查 CRL、OCSP),会影响吞吐;策略类校验应轻量、内存内完成。
校验逻辑放在 Lua 中,既保留了 Nginx 的高性能 TLS 协议栈,又赋予你灵活定义“合法”的能力。关键是把好两道关:Nginx 做信任链验证,Lua 做身份与策略验证。


















