前言:在现代企业架构中,多域名反向代理是实现业务隔离、品牌独立的常见方案。然而,看似简单的Nginx配置背后,隐藏着与TLS协议、后端认证逻辑深度绑定的细节陷阱。本文将从原理到实践,详解为何在多域名场景下,proxy_ssl_name
不能使用环境变量而必须写死,以及这一配置错误如何导致“网页能打开但登录失败”的诡异现象。
一、场景与背景:多域名反代的典型需求
某企业为实现品牌隔离,部署了一套后端服务,通过两个域名app.brandA.com
和app.brandB.com
对外提供服务。架构上,用户请求先经过Nginx反向代理,再转发至后端HTTPS服务(端口27777),整体流程如下:
用户 → Nginx反代 → 后端HTTPS服务(27777端口)
为简化配置,运维团队最初在Nginx中使用单server
块配置多域名,并通过环境变量动态设置proxy_ssl_name
,配置片段如下:
server {
listen 443 ssl;
server_name app.brandA.com app.brandB.com;
ssl_certificate /etc/nginx/ssl/common.crt; # 包含两个域名的SAN证书
ssl_certificate_key /etc/nginx/ssl/common.key;
location / {
proxy_pass https://backend:27777;
proxy_set_header Host $server_name;
proxy_ssl_name $server_name; # 此处使用环境变量
proxy_ssl_server_name on;
}
}
初期现象:两个域名的静态资源(如图片、CSS)均可正常加载,网页能打开;但用户尝试登录时,后端始终返回“账号不存在”或“认证失败”,且仅多域名配置时出现,单域名配置(仅app.brandA.com
)完全正常。
二、核心原理:proxy_ssl_name与TLS握手的“生死时速”
要理解问题根源,需先明确proxy_ssl_name
的作用,以及它在TLS握手过程中的关键地位。
1. TLS握手与SNI协议
当客户端通过HTTPS访问服务时,需经历TLS握手过程,其中SNI(Server Name Indication) 是实现“一台服务器托管多域名HTTPS服务”的核心机制。简单来说:
- 客户端在TLS握手的第一个消息(ClientHello)中,会携带
server_name
字段,告诉服务器“我要访问的域名是XX”; - 服务器根据该字段,返回对应域名的证书(避免多域名场景下证书不匹配的问题);
- 若SNI不匹配,服务器可能返回默认证书,导致客户端证书校验失败(如浏览器提示“不安全”)。
2. proxy_ssl_name的真实作用
在Nginx反向代理场景中,proxy_ssl_name
的作用是:当Nginx作为客户端,向后端HTTPS服务发起TLS握手时,指定发送给后端的SNI值。
也就是说,proxy_ssl_name
直接决定了后端服务收到的“客户端要访问的域名”,进而影响后端返回的证书、以及基于域名的业务逻辑(如租户识别、权限校验)。
3. 环境变量的“时序陷阱”
Nginx中的环境变量(如$server_name
、$http_host
)需要在请求处理过程中动态解析,而TLS握手是在请求转发前的“前置步骤”——此时请求尚未完全解析,环境变量可能无法被正确读取,或读取到非预期值。
例如:
- 若
$server_name
解析延迟,TLS握手时可能传递空值或默认域名,导致后端使用错误证书; - 多域名场景下,变量解析可能出现“串域”(如访问
app.brandB.com
时,SNI被错误设置为app.brandA.com
)。
三、问题深析:为何网页能打开但登录失败?
这一矛盾现象的核心在于:静态资源加载与用户登录依赖后端的不同逻辑。
1. 静态资源加载:不依赖域名绑定
网页的静态资源(图片、JS、CSS)通常是“无状态”的,后端对这类请求的处理逻辑简单:只要请求格式正确、TLS握手成功,就直接返回资源,不验证域名与业务的绑定关系。
因此,即使proxy_ssl_name
传递的SNI偶发错误,只要TLS握手未完全失败(如后端返回默认证书且客户端兼容),静态资源仍能加载,表现为“网页能打开”。
2. 用户登录:深度依赖域名-租户绑定
用户登录接口是“有状态”的,尤其在多租户系统中,后端会通过以下逻辑验证身份:
- 域名→租户映射:后端通过SNI获取的域名(即
proxy_ssl_name
传递的值),查询对应的租户ID(如app.brandA.com
对应租户1,app.brandB.com
对应租户2); - 租户→账号校验:根据租户ID,到该租户的数据库中查询用户账号(如
user@brandA.com
仅存在于租户1的数据库); - 返回认证结果:若域名无法映射到租户,或租户数据库中无此账号,则返回“账号不存在”。
当proxy_ssl_name
使用环境变量导致SNI传递错误时(如app.brandB.com
的请求被映射到租户1),后端在租户1的数据库中找不到user@brandB.com
,自然返回登录失败。
四、排查过程:从现象到本质的定位
1. 初步排查:排除基础配置错误
- DNS与解析:确认两个域名均正确解析到Nginx服务器IP,
nslookup app.brandA.com
和nslookup app.brandB.com
结果正常; - 证书有效性:通过
openssl x509 -in common.crt -noout -text
检查证书,确认两个域名均在SAN扩展中,排除证书本身问题; - Nginx日志:
access.log
显示两个域名的请求均正常到达,error.log
无明显TLS握手错误,排除基础连接问题。
2. 关键验证:对比单/多域名的SNI传递
使用openssl s_client
模拟Nginx向后端发起TLS握手,观察SNI值:
# 测试单域名配置(正常)
openssl s_client -connect backend:27777 -servername app.brandA.com
# 输出中可见:Server Name: app.brandA.com(正确)
# 测试多域名配置(异常)
openssl s_client -connect backend:27777 -servername app.brandB.com
# 输出中可见:Server Name: app.brandA.com(错误,被串域)
结果证实:多域名配置下,proxy_ssl_name $server_name
未能正确传递SNI,导致后端始终收到默认域名。
3. 后端日志佐证:租户识别失败
查看后端服务日志(以Java为例),发现关键错误:
2023-10-01 10:00:00 [ERROR] TenantService - Domain 'app.brandA.com' not mapped to tenant for request from 'app.brandB.com'
日志明确显示:后端收到的SNI是app.brandA.com
,但实际请求来自app.brandB.com
,租户映射失败,导致登录时账号查询无结果。
五、解决方案:多域名单独配置,proxy_ssl_name写死
核心修复思路是:放弃环境变量,为每个域名单独配置server
块,并将proxy_ssl_name
写死为对应域名,确保SNI传递准确。
1. 具体配置
# 域名A配置
server {
listen 443 ssl;
server_name app.brandA.com;
ssl_certificate /etc/nginx/ssl/common.crt;
ssl_certificate_key /etc/nginx/ssl/common.key;
location / {
proxy_pass https://backend:27777;
proxy_set_header Host $server_name;
proxy_ssl_name app.brandA.com; # 写死为当前域名
proxy_ssl_server_name on;
}
}
# 域名B配置
server {
listen 443 ssl;
server_name app.brandB.com;
ssl_certificate /etc/nginx/ssl/common.crt;
ssl_certificate_key /etc/nginx/ssl/common.key;
location / {
proxy_pass https://backend:27777;
proxy_set_header Host $server_name;
proxy_ssl_name app.brandB.com; # 写死为当前域名
proxy_ssl_server_name on;
}
}
# 80端口强制HTTPS
server {
listen 80;
server_name app.brandA.com app.brandB.com;
return 301 https://$server_name$request_uri;
}
2. 配置解析
- 拆分
server
块:每个域名独立配置,避免环境变量在多域名间的解析冲突; proxy_ssl_name
写死:直接指定当前server_name
对应的域名,确保TLS握手时SNI传递准确;- 复用证书:若证书包含多个域名(如SAN证书),可复用证书文件,无需额外申请。
六、验证:确认修复效果
1. TLS握手验证
再次使用openssl
测试,确认SNI正确传递:
# 测试域名A
openssl s_client -connect backend:27777 -servername app.brandA.com
# 输出:Server Name: app.brandA.com(正确)
# 测试域名B
openssl s_client -connect backend:27777 -servername app.brandB.com
# 输出:Server Name: app.brandB.com(正确)
2. 业务功能验证
- 登录测试:分别使用
app.brandA.com
和app.brandB.com
登录,后端日志显示租户映射正确,登录成功; - 功能覆盖:测试核心业务接口(如数据提交、权限验证),确认均能基于正确租户处理请求。
七、经验总结:Nginx多域名反代的避坑指南
proxy_ssl_name
的“静态优先”原则
涉及TLS握手的指令(如proxy_ssl_name
、ssl_certificate
),应优先使用静态值(写死),避免依赖环境变量。这类指令的执行时机早于请求解析,变量可能无法正确生效。多域名配置的“隔离性”
即使域名共享后端服务,也建议拆分server
块单独配置。这种方式虽然增加了配置量,但能避免变量冲突、简化排查,尤其适合多租户场景。证书与SNI的匹配性
若使用单证书支持多域名,需确保证书的SAN扩展包含所有域名;若使用通配符证书(如*.brandA.com
),需确认proxy_ssl_name
传递的域名符合通配符规则。日志与测试工具的关键作用
排查时,openssl s_client
(验证SNI)、后端业务日志(验证租户映射)、Nginx的error_log
(开启debug
级别)是定位问题的三大核心工具。
结语
Nginx反向代理的配置细节,往往与底层协议(如TLS)、后端业务逻辑深度耦合。“proxy_ssl_name不能用环境变量”看似是一个简单的配置规则,实则是对TLS握手时序、SNI作用及多租户认证逻辑的综合考量。在多域名场景中,保持配置的“确定性”,往往是避免诡异问题的最佳实践。