3.2 虚拟主机:一台服务器的多重身份 本节摘要:虚拟主机让一台 httpd 同时伺候几十上百个网站。本节讲三种形态——按域名、按端口、按 IP——各自的判定依据与配置写法,重点拆解基于域名的匹配优先级与默认站点兜底逻辑,以及 HTTPS 时代 SNI 如何让"一个 IP 多张证书"成为可能。读完你能独立配置多站点并解释"敲 IP 为什么打开了个不相干的网站"。 学习目标 阅读完本节,你应当能够: 写出基于域名的虚拟主机完整配置并解释每个字段; 说明匹配优先级与默认站点的兜底行为; 配置基于端口与基于 IP 的形态并说出各自适用场景; 解释 SNI 的工作前提与常见证书错配故障; 用最少停机时间在单机上加一个新站点。 三个问题:找谁、按什么找、找不到给谁 虚拟主机机制回答三个问题。
本节摘要:虚拟主机让一台 httpd 同时伺候几十上百个网站。本节讲三种形态——按域名、按端口、按 IP——各自的判定依据与配置写法,重点拆解基于域名的匹配优先级与默认站点兜底逻辑,以及 HTTPS 时代 SNI 如何让"一个 IP 多张证书"成为可能。读完你能独立配置多站点并解释"敲 IP 为什么打开了个不相干的网站"。
阅读完本节,你应当能够:
虚拟主机机制回答三个问题。找谁:请求头里的 Host 字段是唯一身份凭证。按什么找:配置里的每个 VirtualHost 容器声明自己认领的地址端口组合与 ServerName(必要时加 ServerAlias)。找不到给谁:该地址端口上第一个定义的虚拟主机充当默认站点,接收所有没人认领的请求——这就是"直接敲服务器 IP 会打开一个不相干网站"的原因:IP 访问没有 Host 头(或 Host 是 IP),无人认领,落到默认站点。
理解了第三个问题,你就理解了一个安全惯例:默认站点应该配置成拒绝或返回一个不含内容的占位页,而不是让它"顺便"展示某个真实站点:
# 第一个虚拟主机 = 默认站点,专门用来"接垃圾" <VirtualHost *:80> ServerName default.invalid <Location /> Require all denied </Location> </VirtualHost> # 真实站点从第二个开始 <VirtualHost *:80> ServerName www.example.com ServerAlias example.com DocumentRoot /var/www/example ErrorLog ${APACHE_LOG_DIR}/example-error.log CustomLog ${APACHE_LOG_DIR}/example-access.log combined </VirtualHost>
上面就是基于域名的形态,关键点逐个说:
上线前验证匹配(不动 DNS 的本地复现):
apachectl -t -D DUMP_VHOSTS
输出片段:
*:80 default.invalid *:80 www.example.com example.com
列表顺序即优先级顺序,第一个就是默认站点。确认无误后 reload,再用 curl 伪造 Host 头做端到端验证。
新站点上线流程压缩成四步:建目录与权限 → 写虚拟主机片段 → a2ensite(或放入 conf.d)→ apachectl -t && apachectl graceful。全程 graceful,线上已有连接不断。
基于端口:同一域名不同端口对应不同站点。8443 跑管理后台、8080 跑测试环境是常见用法。注意两点:必须先 Listen 8080 声明监听;用户侧要显式敲端口,不适合对外业务,适合内部工具。
基于 IP:机器有多个 IP,每个 IP 一个站点,靠目标 IP 而非 Host 判定。它比基于域名更"老",价值在于协议不合作也能区分——某些老客户端不发 Host 头,或需要在 IP 层做隔离(不同安全级别的站点绑不同网卡)。IPv4 地址枯竭后,公网场景基本被基于域名加 SNI 取代,内网多网卡环境偶有使用。

虚拟主机判定的传统依据是 HTTP 层的 Host 头,但 HTTPS 请求进来时,TLS 握手发生在 HTTP 之前——服务器必须先出示证书,才能解密看到 Host。那一个 IP 上多个 HTTPS 站点怎么各自出示正确的证书?
答案是 SNI(服务器名称指示):现代客户端在 TLS 握手的第一条消息里,以明文扩展字段带上目标域名。服务器据此挑选对应虚拟主机的证书完成握手。配置形态:
Listen 443 <VirtualHost *:443> ServerName shop.example.com DocumentRoot /var/www/shop SSLEngine on SSLCertificateFile /etc/ssl/shop/fullchain.pem SSLCertificateKeyFile /etc/ssl/shop/privkey.pem </VirtualHost> <VirtualHost *:443> ServerName blog.example.com DocumentRoot /var/www/blog SSLEngine on SSLCertificateFile /etc/ssl/blog/fullchain.pem SSLCertificateKeyFile /etc/ssl/blog/privkey.pem </VirtualHost>
SNI 的前提是客户端与服务端双向支持(现代浏览器与 2.4 都默认支持,只剩极老的 Windows XP 时代的客户端不支持)。不支持 SNI 的客户端会拿到默认站点的证书,于是出现"证书域名不匹配"警告。
由此衍生一个高频故障:证书与虚拟主机没配对。症状是浏览器访问 A 站弹出 B 站的证书告警。排查顺序:DUMP_VHOSTS 确认站点被认领 → 检查证书文件路径是否张冠李戴 → 用 openssl 直连看实际出示的证书:
echo | openssl s_client -connect 127.0.0.1:443 -servername blog.example.com 2>/dev/null | grep subject # 预期输出 subject=CN = blog.example.com,若出现别的域名即配对失败
-servername 参数就是在模拟 SNI,这个命令等于"扮演浏览器复现握手"。
**注意一个高频坑:虚拟主机改了配置不 reload,或 reload 前忘了 -t 检查——语法错误的配置 graceful 会失败,但失败信息只在终端与错误日志里,"改了没生效"的第一嫌疑永远是自己没重启成功。
单机站点数一多,三个工程问题会浮出来。重复:几十个站点配置 90% 相同,用宏模块(mod_macro)定义模板,参数化域名与路径,改一处全站生效。命名:站点文件名与 ServerName 保持一致,排错时文件名即域名。日志切分:每站独立日志已就位,配合第 6 章的日志轮转,避免单文件无限膨胀。
另一个方向的问题是"何时该分机器":单机虚拟主机再多,MPM 的进程与内存也是所有站点共享的水池——一个大流量站点打满水池,邻居全部遭殃。隔离需求(一个站点拖不垮别人)比容量需求更早触发拆分。
💡 一句话记住本节:虚拟主机的全部逻辑就是一场认领——谁报上 Host,谁就拿到工单;没人报,第一个站点被迫接锅。
下一节进入旅程第五站的岔路口:重写引擎如何改变请求的行进方向。