云锁防盗链按 Referer 拦截样式表的排查与修复 · Windows Server 2019 + XAMPP + WordPress + Super Static Cache 实战笔记
index.html.gz 解压坏了,不是 CSP,不是缓存按域名存了两份。根因是 https://511yj.com 在 443 上没有 301 跳到 www,浏览器带着裸域 Referer 去 www 取 CSS,被云锁防盗链回了一份「网站防火墙」HTML;响应头又有 nosniff,浏览器拒绝把它当样式表,于是整页一个样式都不生效。修法是在主站 .htaccess 里补三行 301,把 Host 统一到 www。
一 现象
- 无痕窗口打开
https://511yj.com:文字正常,整页没有 CSS 样式。 - 无痕窗口打开
https://www.511yj.com:样式齐全。 - 普通窗口看着「两个都正常」,是因为浏览器早先从 www 页面缓存过这些 CSS。
- 站点启用了 Super Static Cache,首页由
C:\xampp\htdocs\super-static-cache\index.html.gz提供,曾怀疑是解压后的index.html出了问题。
.htaccess 里写了 ExpiresByType text/css "access plus 1 year",CSS 浏览器缓存一整年,普通窗口根本不重新发请求。
二 先把「解压坏了」排除掉
贴出来的首页源码里,所有样式表都是硬编码的 www 绝对地址:
<link rel='stylesheet' id='aldehyde-style-css' href='https://www.511yj.com/wp-content/themes/aldehyde/style.css?ver=1791971959' media='all' /> <link rel='stylesheet' id='aldehyde-bootstrap-style-css' href='https://www.511yj.com/wp-content/themes/aldehyde/assets/bootstrap/css/bootstrap.min.css?ver=1791953997' media='all' /> <link rel="canonical" href="https://www.511yj.com" />
没有相对路径、没有裸域地址。页面文字也正常渲染,不是 .gz 没解干净那种整页二进制乱码。解压这一步无罪。
三 逐层排除:一张被推翻的假设表
| 假设 | 验证命令 / 读数 | 结论 |
|---|---|---|
| 静态缓存按 Host 存了两份,裸域那份是旧的 | dir /s /b C:\xampp\htdocs\*index.html.gz 只有一份 super-static-cache\index.html.gz,其余是 archives / guestbook / sitemap 等子页 |
否 |
| 裸域返回的 HTML 里 CSS 写成裸域或相对地址 | 两域分别 curl.exe -s --compressed https://511yj.com/ | findstr /i /c:"<link" 与 www 同命令,输出逐字一致,全是 www 绝对地址 |
否 |
| 裸域多一条 CSP 把 www 样式表判成跨源 | curl.exe -sI ... | findstr /i /c:"content-security",两边都只有 upgrade-insecure-requests;,不限 style-src |
否 |
| CSS 文件本身被重写吞成 HTML | curl.exe -sIL -o nul -w "code=%{http_code} type=%{content_type}\n" https://511yj.com/wp-content/themes/aldehyde/style.css,两域都 200 text/css |
否 |
| 443 层没有把裸域规范化到 www | 两域 curl.exe -sI https://.../ | findstr /i /c:"location" 都没有 Location |
是,这是起点 |
| WAF / 防盗链按 Referer 拦跨子域静态资源 | 带 Referer 对照(见下一节):裸域 Referer 拿到 text/html 5758 字节「网站防火墙」页,www Referer 拿到 text/css 8982 字节 |
是,实锤 |
curl.exe -sIL .../style.css 测的是「不带 Referer 时那个文件能不能取到」,它证明不了「裸域页面里嵌的那条 www CSS,浏览器放不放行」。这两件事不是一回事。排查时差点在这里绕死。
四 实锤:带 Referer 的对照实验
关键动作是手动带上 Referer,模拟真实浏览器。在本地电脑 CMD 里跑(别在 ECS 上跑,原因见第七节):
curl.exe -s -o nul -w "ref=naked : code=%{http_code} type=%{content_type} size=%{size_download}\n" -H "Referer: https://511yj.com/" -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "https://www.511yj.com/wp-content/themes/aldehyde/style.css?ver=1791971959"
curl.exe -s -o nul -w "ref=www : code=%{http_code} type=%{content_type} size=%{size_download}\n" -H "Referer: https://www.511yj.com/" -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "https://www.511yj.com/wp-content/themes/aldehyde/style.css?ver=1791971959"
| Referer | 读数 | 实际内容 |
|---|---|---|
https://511yj.com/ |
code=200 type=text/html size=5758 |
拦截页 |
https://www.511yj.com/ |
code=200 type=text/css size=8982 |
真正的样式表 |
把裸域那条返回体抓出来看:
curl.exe -s -H "Referer: https://511yj.com/" -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "https://www.511yj.com/wp-content/themes/aldehyde/style.css?ver=1791971959" | findstr /i /c:"防火墙" /c:"<html" /c:"<!DOCTYPE"
返回的是 <!DOCTYPE html ...>、标题 网站防火墙、带 yunsuologo、正文写着「您的行为被识别为盗链,已被管理员 [www.511yj.com] 设置拦截」。FINDSTR: 写入错误 只是管道输出太长被中途断开,前面已经命中,不影响结论。
style.css 的页面,曾被反复当成「抓取通道超时」的噪音清掉。它们一直就是真证据。日志里看着像噪音的东西,先确认它命中的 URL 再下结论。
五 完整机制链
- 输入
https://511yj.com,443 的 vhost 不跳,Host 保持裸域。:80那条 301 带RewriteCond %{HTTPS} off,管不到 https。 - 返回共享的那份静态首页,里面 CSS 全部硬编码成
https://www.511yj.com/...。 - 浏览器带
Referer: https://511yj.com/向 www 请求样式表。 - 云锁「网站防火墙」防盗链判定为跨站盗链,回 5758 字节 HTML 拦截页,HTTP 状态仍是
200(所以按状态码查会漏掉)。 - 响应头有
X-Content-Type-Options: nosniff,浏览器拒绝把text/html当text/css应用 → 整页无样式,而不是缺一两个文件。 - www 页面同源、Referer 是 www → 放行,拿到真 CSS。
- 普通模式因一年期 CSS 缓存命中,裸域页根本不发这个请求 → 看着正常;无痕没缓存 → 必撞。
「两边 HTML 完全相同却一个有样式一个没有」,分叉不在 HTML,在浏览器发 CSS 请求时那个 Referer。
六 为什么原来的「HTTPS 强制跳转」没生效
原片段:
# ---- HTTPS 强制跳转(必须排在缓存规则之前)----
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} ^(www\.)?511yj\.com$ [NC]
RewriteRule ^(.*)$ https://www.511yj.com%{REQUEST_URI} [L,R=301]
RewriteRule ^index\.php$ - [L]
第一行写的是「只有非 HTTPS 才执行」。用户输的是 https://,条件不成立,整条规则被跳过。于是裸域在 443 上原样返回页面,地址栏还停在 511yj.com。
七 修复:.htaccess 补三行 301
C:\xampp\htdocs\.htaccess 这一份就够。443 的响应里带着 Content-Security-Policy 和 X-Content-Type-Options: nosniff,这两个头正是这份文件里 SetEnvIf Host 那段发的,说明 https 请求确实走它,不用翻 httpd-ssl.conf。
先备份:
copy C:\xampp\htdocs\.htaccess C:\xampp\htdocs\.htaccess.bak
把第六节那段整段替换成下面这样,位置保持在 # BEGIN WordPress 块内、HTTPS 强跳之后、Super Static Cache 之前:
# ---- HTTP → HTTPS + www(原来这条,一个字不动)----
RewriteCond %{HTTPS} off
RewriteCond %{HTTP_HOST} ^(www\.)?511yj\.com$ [NC]
RewriteRule ^(.*)$ https://www.511yj.com%{REQUEST_URI} [L,R=301]
# ---- HTTPS 但仍是裸域 → 补跳 www(新增)----
RewriteCond %{HTTPS} on
RewriteCond %{HTTP_HOST} ^511yj\.com$ [NC]
RewriteRule ^(.*)$ https://www.511yj.com%{REQUEST_URI} [L,R=301]
RewriteRule ^index\.php$ - [L]
三个容易抄错的点
- 条件要跟着新行重写一遍。
RewriteCond只管紧挨着它的那一条RewriteRule,不会继承上面那组,所以新行前面必须再写两条 Cond。 - 第二条 Host 正则不要带
(www\.)?。写成^(www\.)?511yj\.com$的话,https://www.511yj.com也会命中、自己跳自己,直接无限重定向。只匹配纯裸域^511yj\.com$。 - 沿用
%{REQUEST_URI}即可,路径和查询串都会带过去,不用加QSA。
逻辑就两条并列:http 来的 → 上一条跳走;https 但裸域 → 新条跳走;https://www.511yj.com 两条都不命中,正常往下走静态缓存。
.htaccess 即时生效,不用重启 Apache,其余响应头、Expires、Super Static Cache、/weixin/ 例外、WordPress 兜底全不动。
运行时前提:第二条依赖 %{HTTPS} 在 443 vhost 里确实等于 on。标准 mod_ssl + SSLEngine on 下成立;XAMPP 个别配置会让它恒为 off。若验证时 301 没出来,不用改别的,把 RewriteCond %{HTTPS} on 这一行直接删掉即可 —— 剩下 ^511yj\.com$ 一条就够,http 裸域已被第一条 [L] 抢先跳走。二选一,别两版叠加。
八 验证:三条读数,缺一不可
curl.exe 被本机防护按进程拦,会直接吐「拒绝访问」,那是 Windows 在创建进程那一层就拒了,零网络信息量。网络不通的正常表现是 code=000 或超时,绝不会是「拒绝访问」。
curl.exe -sI https://511yj.com/ | findstr /i /c:"HTTP/" /c:"location"
curl.exe -sI https://www.511yj.com/ | findstr /i /c:"HTTP/" /c:"location"
curl.exe -sIL -o nul -w "final=%{url_effective} code=%{http_code}\n" https://511yj.com/
| 命令 | 期望 | 说明 |
|---|---|---|
| 第一条 | HTTP/1.1 301 Moved Permanently + Location: https://www.511yj.com/ |
裸域补跳生效 |
| 第二条 | HTTP/1.1 200 OK、没有 Location |
专门验没有自跳循环 |
| 第三条 | final=https://www.511yj.com/ code=200 |
最终落到 www |
最直接的一条其实是浏览器:开个新的无痕窗口(别用之前那个,避免 301 缓存干扰)打开 https://511yj.com,地址栏跳成 www、样式齐全,就结束了。
ECS 上只用只读命令确认文件真落盘了(备份 ≠ 已写入):
findstr /i /c:"补跳 www" /c:"%{HTTPS} on" C:\xampp\htdocs\.htaccess
能看到那三行才算改好。.htaccess.bak 会被 <FilesMatch "^\.ht"> 挡住,不会被下载,备份留着放心。
九 踩坑集:包括最后那两条 curl 报错
坑 1 · 把文档里的省略号原样粘进 CMD
复测时执行了这样一行:
C:\Users\zcp52>curl.exe ... -H "Referer: https://511yj.com/" ...style.css curl: (3) URL rejected: Bad hostname curl: (6) Could not resolve host: ...style.css
原因很简单:教程里写的 ... 是省略占位,表示「中间那一长串参数」,不是命令的一部分。原样粘进 CMD 后,curl 把 ... 当成字面量:
(3) URL rejected: Bad hostname——...不是合法主机名,URL 解析阶段就被拒了。(6) Could not resolve host: ...style.css——...style.css被当成另一个 URL 去解析,DNS 里当然没有这个主机。
这不是网站问题、不是 DNS 问题、跟 ECS 的 DNS 指向 127.0.0.1 也毫无关系。要复测就贴第四节里带完整 URL 的那两条命令。
坑 2 · 修复后别再用「手工伪造裸域 Referer」自证失败
即使把 URL 写全,这条命令在修复后大概率仍返回 text/html 5758 —— 因为你人为造出了一个真实浏览器现在根本不会发出的 Referer。云锁按 Referer 判断,它拦得没错。判断修复是否成功,只看 301 的 Location 和无痕窗口,不要用这条命令推翻结论。
坑 3 · 两份首页快照不能当判据
抓取服务里 511yj.com 那份是完整渲染(含导航、新文列表),www 那份是正文摘要 —— 这是解析深度不同,且它记的是原始请求 URL、不记 301 跟随结果。301 属于 HTTP 响应头,只认 Location,正文内容一律不看。修复后裸域连内容都不会单独返回,更不存在「两份 HTML」。
坑 4 · .htaccess 里看着像重复的地方,别顺手清理
RewriteEngine On/RewriteBase /出现两次(WordPress 外层 + Super Static Cache 内层),是插件生成的冗余,Apache 重复声明无害。<IfModule mod_rewrite.c>套了一层,恒为真,能跑。<Ifmodule mod_headers.c>大小写混用、header set小写,Apache 指令不区分大小写。<IfModule mod_headers.c>在顶部和缓存段各出现一次,是并列不是嵌套,顶部那个已闭合。SetEnvIf属于mod_setenvif却写在mod_headers判断里 —— 能工作但不规范,两个模块默认都加载,实测响应头一直在。改它收益为零、风险不为零,别碰。
坑 5 · 可选优化,不是故障原因
ExpiresByType text/css "access plus 1 year" 建议顺手改短(比如 1 个月),以后再遇到资源类问题不用靠无痕才能发现。跟这次无样式没有因果关系,不急。
十 与证书续订的关系(附一句确认)
这次改动和 win-acme 续期不是一层:证书 / ECN / 出站链路 / junction 属于网络与文件层,本问题属于 Host 规范化 + WAF 防盗链 + 响应头 层,别混着查。
唯一需要确认的:ACME 的 /.well-known/acme-challenge/ 会被这个 301 带到 www,Let's Encrypt 允许跟随 301;之前建的目录联接让 C:\xampp\htdocs 与 C:\xampp\htdocs\bbs 读的是同一份 token 文件,自测过 bbs=200 size=4。不影响下次自动续期,Certificates_bak 保持改名别还原。
十一 命令速查
| 用途 | 命令 | 在哪跑 |
|---|---|---|
| 备份配置 | copy C:\xampp\htdocs\.htaccess C:\xampp\htdocs\.htaccess.bak |
ECS |
| 确认新增规则落盘 | findstr /i /c:"补跳 www" /c:"%{HTTPS} on" C:\xampp\htdocs\.htaccess |
ECS |
| 裸域是否 301 | curl.exe -sI https://511yj.com/ | findstr /i /c:"HTTP/" /c:"location" |
本地电脑 |
| www 是否无自跳循环 | curl.exe -sI https://www.511yj.com/ | findstr /i /c:"HTTP/" /c:"location" |
本地电脑 |
| 跟随跳转后的最终 URL | curl.exe -sIL -o nul -w "final=%{url_effective} code=%{http_code}\n" https://511yj.com/ |
本地电脑 |
| 两域首页 link 对比 | curl.exe -s --compressed https://511yj.com/ | findstr /i /c:"<link" |
本地电脑 |
| 响应头对比 | curl.exe -sI https://511yj.com/ | findstr /i /c:"content-security" /c:"content-encoding" /c:"x-content-type" /c:"location" |
本地电脑 |
| Referer 对照实验(修复前定位用) | 见第四节两条带 -H "Referer: ..." 的完整命令 |
本地电脑 |
| 缓存文件清点 | dir /s /b C:\xampp\htdocs\*index.html.gz 2>nul |
ECS |
index.html.gz。
本文所有结论均来自本机 CMD 实测读数;被推翻的假设也一并保留,便于下次遇到同类「两边看起来一样却表现不同」的问题时对照。