编程 OpenResty 1.31.1.1 落地:precontent 阶段、按请求校验上游证书与 cosocket TLS

2026-10-17 21:09:03

OpenResty 1.31.1.1 落地:precontent 阶段、按请求校验上游证书与 cosocket TLS

发布说明与 changelog 见 OpenResty 1.31.1.1 正式发布,指令细节以 lua-nginx-module README 为准。这一版把 nginx 内核从 1.29.2 升到 1.31.1,OpenSSL 3.5.5 → 3.5.6,lua-nginx-module 到 v0.10.31。对写配置的人影响最直接的,是下面这几处新增能力。

一、precontent_by_lua:内容处理前多插一段 Lua

lua-nginx-module v0.10.31 新增 precontent_by_lua*。阶段顺序变成:

rewrite_by_lua* → access_by_lua* → precontent_by_lua* → content(proxy_pass / 静态文件 / content_by_lua*)

几个指令的分工,可以按"此时请求走到哪一步"来理解:

  • rewrite_by_lua*:最早,适合改 URI、设变量、整条请求级别的重定向;
  • access_by_lua*:访问控制,鉴权、IP 黑白名单,和 nginx 自带的 allow/deny 同阶段;
  • precontent_by_lua*:新增,位于 access 之后、真正执行 proxy_pass、静态文件或 content_by_lua* 之前,用来做鉴权收尾、限流扣减、上游路径改写这类"已经确定要进内容处理"的动作;
  • content_by_lua*:内容处理器本身,负责产生响应体。

示例:

location /api/ {
    access_by_lua_block {
        -- 粗粒度拦截放在 access
        if ngx.var.binary_remote_addr == "10.0.0.99" then
            return ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    }

    precontent_by_lua_block {
        local token = ngx.req.get_headers()["Authorization"]
        if not token then
            ngx.log(ngx.WARN, "missing auth, uri=", ngx.var.uri)
            return ngx.exit(ngx.HTTP_UNAUTHORIZED)
        end

        local limit = ngx.shared.api_limit
        local key = "rl:" .. ngx.var.binary_remote_addr
        local n, err = limit:incr(key, 1, 0, 60)
        if not n then
            ngx.log(ngx.ERR, "limit incr failed: ", err)
            return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end
        if n > 100 then
            return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
        end

        -- 改写交给上游的路径
        ngx.req.set_uri("/internal" .. ngx.var.uri, false)
    }

    proxy_pass http://app_backend;
}

注意点:

  • 不要在这个阶段输出响应体。ngx.say/ngx.print 属于内容处理,放在这里和 proxy_pass 冲突;拒绝请求用 ngx.exit(状态码),返回信息可以放 header 或用 content_by_lua* 承接。
  • ngx.exit 与 proxy_pass 的配合:ngx.exit(ngx.HTTP_UNAUTHORIZED) 之后请求即终止,后面的 proxy_pass 不会执行;想放行就正常返回 handler,不要调 ngx.exit。ngx.exit(ngx.OK) 在 precontent 阶段的语义按模块 README,别按 rewrite 阶段的习惯套。
  • 执行条件按模块 README 确认:哪些 location 上下文生效、内部重定向和 upstream 重试时各执行几次,上线前用 error_log ... debug 打点确认,不要想当然。

二、proxy_ssl_verify_by_lua*:按请求决定上游证书校验

proxy_ssl_verify / proxy_ssl_verify_depth 是静态指令,一条 location 只能有一种设置。网关里如果有多个上游、其中一部分还在用自签证书,过去只能整条 location 关校验或者拆 location。新增的 proxy_ssl_verify_by_lua* 让校验开关和校验深度按请求决定。

示意配置:

location /upstream/ {
    set $verify_upstream "on";
    set $verify_depth 2;

    proxy_ssl_verify $verify_upstream;
    proxy_ssl_verify_depth $verify_depth;
    proxy_ssl_trusted_certificate /etc/ssl/certs/internal-ca.pem;

    proxy_ssl_verify_by_lua_block {
        local host = ngx.var.upstream_host or ""
        if host == "legacy.internal" then
            ngx.var.verify_upstream = "off"
        else
            ngx.var.verify_upstream = "on"
            ngx.var.verify_depth = 1
        end
    }

    proxy_pass https://$upstream_host;
}

proxy_ssl_verify 是否接受变量、by_lua 块里设置的结果以何种方式映射到校验开关与深度,以模块 README 里的示例为准;上面这段只示意"在 handler 里按上游名分支"这个结构。生产上不建议对自签名上游直接 off,更稳的做法是给它单独挂一份 CA:

proxy_ssl_trusted_certificate /etc/ssl/certs/legacy-ca.pem;
proxy_ssl_verify_depth 1;

三、tcpsock:getsslsession 与 settrustedstore:可信 CA 下沉到单次握手

lua_ssl_trusted_certificate 是模块级全局配置,一个 worker 里所有 cosocket TLS 握手共用同一条信任链。v0.10.31 之后可以用 settrustedstore 给单次握手指定可信 CA,getsslsession 拿到握手后的 session 供后续连接复用。

local sock = ngx.socket.tcp()
sock:settimeout(3000)
-- 本次握手只信这份 CA,不依赖全局 lua_ssl_trusted_certificate
sock:settrustedstore("/etc/ssl/certs/internal-ca.pem")

local ok, err = sock:connect("10.0.0.21", 8443)
if not ok then
    ngx.log(ngx.ERR, "connect failed: ", err)
    return
end

local session, err = sock:getsslsession()
if session then
    ngx.shared.tls_sessions:set("up:10.0.0.21", session, 300)
end

settrustedstore 要在握手前调用;getsslsession 在握手完成后取 session。session 如何回填到下一次握手(复用接口名与调用时机)、settrustedstore 与模块级配置的优先级,以模块文档为准。这套组合适合网关里同时对接公有 CA 上游和自签 mTLS 上游的场景:不用为了一个自签上游把全局信任链放宽。

四、keepintvl / keepcnt:TCP keepalive 按连接设置

changelog 里给 TCP 套接字新增了 keepintvl 和 keepcnt 两个选项,对应内核的 TCP_KEEPINTVL(探测间隔)和 TCP_KEEPCNT(探测次数)。以往这两个值只能在系统 sysctl 或 nginx 主配置层面统一,现在可以落到单条 cosocket 连接上。

指令名、参数位置(是建连参数还是 nginx 指令默认值)以 lua-nginx-module README 为准,不要照抄别的版本示例。验证时可以起一条长连接后用 ss 看内核定时器:

ss -tnoi '( dport = :6379 )'

输出里的 timer:(keepalive,...) 能反映探测间隔是否按预期生效。

五、升级前自查这几条路径

  • 内核跨了两个 minor(1.29.2 → 1.31.1),第三方 C 模块要按新内核重编,动态模块同样。
  • OpenSSL 3.5.6,涉及自编译或链接系统 OpenSSL 的部署要一起换。
  • 修复集中在 QUIC 连接关闭、worker 关闭、SSL 缓存相关的崩溃。对上号的场景:开了 HTTP/3;worker 退出时还持有 cosocket 或定时器;用了 ssl_session_cache 或在 Lua 里碰过 SSL session 复用。这几条路径上的服务,升级后先小流量跑一轮,重点看 worker 退出时有没有 crash log。
  • lua-resty-core 这版是 v0.1.34rc2,生产按发布包里的实际版本走。
  • LuaJIT 到 v2.1-20260415,新增 ffi.abi("dualnum");ARM64、s390x 和 FFI 有修复,用到 FFI 调用的模块建议回归一遍。
  • 小改动顺手可用的:ngx.header['WWW-Authenticate'] 支持 table 设多值;lua-resty-mysql v0.30 支持 ed25519;xss-nginx-module v0.07 支持动态模块构建。

六、验证方法

用 resty 单独跑 cosocket 逻辑,不用起整个 nginx:

resty -e '
local sock = ngx.socket.tcp()
sock:settimeout(2000)
sock:settrustedstore("/etc/ssl/certs/internal-ca.pem")
print(sock:connect("10.0.0.21", 8443))
'

用 openssl s_client 对照服务端证书链,确认自签 CA 该信谁:

openssl s_client -connect 10.0.0.21:8443 \
  -servername backend.internal \
  -showcerts -CAfile /etc/ssl/certs/internal-ca.pem

阶段执行和校验分支用 debug 日志确认:

error_log logs/error.log debug;
tail -f logs/error.log | grep -E 'precontent|ssl|verify'

手上这套配置改完之后,precontent_by_lua 里 ngx.exit 的分支、proxy_ssl_verify_by_lua* 按上游切换的分支,都要各打一条日志验证过一次再上量。

复制全文 生成海报 OpenResty Nginx Lua 网关 TLS

推荐文章

程序员茄子在线接单