侧边栏壁纸
  • 累计撰写 207 篇文章
  • 累计收到 2 条评论

给网站做了独立静态资源域名,搜索引擎快照却全是排版错乱与白屏?排查爬虫渲染跨域 CORS 拦截、woff2 字体防盗链 403 与 Nginx always 缺失我踩过的几个坑

2026-10-16 / 0 评论 / 10 阅读
AI 摘要由 AI 生成

文章指出,将网站静态资源迁移至独立域名后,搜索引擎抓取出现排版错乱和白屏问题,原因在于爬虫渲染时遇到跨域CORS拦截、woff2字体防盗链403和Nginx配置缺失。作者通过排查,发现并解决了这些问题,确保了搜索引擎正确渲染页面。

上个月我把博客和几个内容站的静态资源全剥离到了独立的 CDN 域名 static.example.com,顺便把 CSS、JS、woff2 字体和配图全同步到了对象存储桶里做动静分离。

本地在 Chrome 里测了几轮,页面加载干脆利落,Lighthouse 跑分也相当漂亮。我当时以为这套动静分离架构算稳了,结果上线差不多十天,Google Search Console(GSC)后台突然跳出一串警告:"移动设备易用性问题:内容宽于屏幕、可点击元素相距过近"。

接着我去百度站长平台的抓取诊断里跑了一次实时抓取,看回传的页面渲染截图:整个页面的标题和正文严重重叠,栅格容器高度全部塌陷,自定义的图标字体全变成了带叉号的豆腐块方块,部分长列表页面甚至直接变成了白底无样式的文字堆叠。

页面在普通用户的浏览器里明明显示正常,为什么一到搜索引擎的抓取渲染器里就彻底散架?

我花了两天时间顺着 Nginx 访问日志、CDN 边缘节点缓存头以及 Headless Chromium 的渲染管线一路排查,抓出了三个直接导致爬虫渲染失败的隐藏配置坑。


爬虫跑的是带完整安全沙箱的无头浏览器

现代搜索引擎(Googlebot、Bingbot 和百度的 Baiduspider-render)抓取页面时,会调用基于 Headless Chromium 的无头浏览器跑完整的页面渲染。

既然是真正的浏览器内核,它就会严格执行 Web 安全标准:

  1. 它会解析 CSSOM 并加载外部字体。
  2. 跨域发起的字体与样式请求,必须受到同源策略(Same-Origin Policy)和 CORS(跨域资源共享)的强约束。
  3. 任何外部子资源如果返回 403、5xx,或者由于跨域头缺失被浏览器沙箱拦截,渲染器不会重试,直接丢弃该资源继续绘制。

当主站域名是 www.example.com,而 CSS、JS、字体挂在 static.example.com 时,对渲染器来说这就是标准的跨域请求。本地用户之所以看起来正常,往往是因为浏览器之前缓存了带有效头的资源,或者本地扩展放宽了限制,而爬虫在抓取集群里每次都是以接近无痕沙箱的状态进行冷启动。


坑一:woff2 字体遭遇 CORS 拦截,Nginx 缺少跨域响应头

最先崩掉的是页面里的字体和图标。

在 Web 标准中,普通图片(<img>)跨域加载通常不受 CORS 限制,但字体文件(.woff、.woff2、.ttf)受到严格的同源策略约束。即便在同一个主域名的不同二级子域之间,只要协议、域名或端口有任何不同,浏览器就会要求服务端必须显式返回 Access-Control-Allow-Origin。

我用 curl 模拟带有主站来源的请求检查静态资源服务器:

curl -I -H "Origin: https://www.example.com" https://static.example.com/assets/fonts/inter-latin.woff2

终端打印出的响应头如下:

HTTP/2 200
server: nginx
date: Tue, 14 Oct 2026 09:12:04 GMT
content-type: font/woff2
content-length: 48924
last-modified: Mon, 13 Oct 2026 03:00:10 GMT
cache-control: max-age=31536000, public
accept-ranges: bytes

整个响应头里根本没有 Access-Control-Allow-Origin。

在无头浏览器渲染管线里,遇到这种情况会直接在内部控制台抛出错误:

Access to font at 'https://static.example.com/assets/fonts/inter-latin.woff2' from origin 'https://www.example.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

一旦字体被直接拦截,页面如果配置了 font-display: block,文本就会处于完全隐形状态;即便退回到系统字体,由于字符宽度和行高差异巨大,整个 CSS 容器的高度瞬间失算,导致元素大面积重叠。

在 Nginx 的静态资源块里,必须针对字体及静态文件补上基础跨域头:

location ~* \.(eot|ttf|woff|woff2|svg)$ {
    add_header Access-Control-Allow-Origin "*" always;
    add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
    add_header Access-Control-Allow-Headers "*" always;
    expires 365d;
    access_log off;
}

注意上面的 always 关键字,这直接引出了第二个更隐蔽的坑。


坑二:add_header 缺少 always,304 协商缓存把跨域头扒光

我在配置里加上了 add_header Access-Control-Allow-Origin "*";,再次用上面的 curl 命令测试,确认返回了 200 并且带上了跨域头。

可两天过去,Google Search Console 的实时测试里依然有部分页面报字体加载失败。

我复盘了爬虫的抓取行为:爬虫如果之前抓过这个资源,后续请求时必然会带上 If-Modified-Since 或 If-None-Match 发起协商缓存检查。

我带上协商缓存头再次发请求测试:

curl -I -H "Origin: https://www.example.com" \
     -H "If-Modified-Since: Mon, 13 Oct 2026 03:00:10 GMT" \
     https://static.example.com/assets/fonts/inter-latin.woff2

终端返回的结果当场让我愣住了:

HTTP/2 304
server: nginx
date: Tue, 14 Oct 2026 09:25:31 GMT
last-modified: Mon, 13 Oct 2026 03:00:10 GMT
cache-control: max-age=31536000, public

原本应该出现的 Access-Control-Allow-Origin: * 居然凭空消失了。

这是 Nginx 一个非常坑的默认机制:add_header 指令默认只对返回状态码为 200, 201, 204, 206, 301, 302, 303, 307 的响应生效。遇到 304 Not Modified、401、403、404、500 等状态码时,Nginx 会默默把所有 add_header 定义的头部全部剔除。

爬虫拿到 304 响应,发现响应里没有跨域允许头,Chromium 渲染引擎按照跨域规范直接阻断了该资源的使用,导致已缓存的字体反而无法加载。

解决办法很简单,给每一行 add_header 末尾都显式追加 always 参数:

# 错误写法:304 协商缓存时该头会被丢弃
add_header Access-Control-Allow-Origin "*";

# 正确写法:任何状态码(包括 304、403、500)均强制输出该头
add_header Access-Control-Allow-Origin "*" always;

只要缺少这个参数,线上静态资源的跨域判定就会陷入时灵时不灵的状态。


坑三:防盗链 valid_referers 误伤爬虫空 Referer 请求

为了防止静态资源域名被恶意站点盗链吃流量,我在 static.example.com 的 Nginx 配置里加了防盗链指令:

# 问题配置
valid_referers server_names *.example.com;
if ($invalid_referer) {
    return 403;
}

这段配置在普通浏览器里跳转访问时看似毫无破绽,因为主站页面发起的资源请求都会携带 Referer: https://www.example.com/...。

但是,搜索引擎爬虫的抓取机制完全不同:

  1. 很多爬虫子资源嗅探集群在并发抓取 CSS、JS、图片时,出于抓取策略或隐私安全考量,发送的 HTTP 请求头里直接没有 Referer。
  2. 也有部分爬虫发送的 Referer 是搜索引擎自身的回溯地址或定制协议(如 android-app://com.google.android.googlequicksearchbox/...),与主站域名并不一致。

因为我的规则里没有声明 none 和 blocked,所有没有 Referer 头的请求全被直接判定为非法的盗链请求,Nginx 当场返回 403 Forbidden。

我拿模拟没有 Referer 的请求直接测试:

curl -I https://static.example.com/assets/css/app.min.css

返回果然是刺眼的 HTTP/2 403。

更致命的是:主站的 CSS 样式文件如果拿到了 403,爬虫拿到的是完全没有任何样式的光秃秃 HTML,所有文字、图片自上而下顺流排布,在移动端视口里宽度直接炸开,立刻被搜索引擎打上移动体验不合格的标签。

针对静态资源域名的防盗链,必须显式放行空 Referer,同时把主流搜索引擎爬虫的来源加入白名单:

location ~* \.(css|js|png|jpg|jpeg|gif|ico|webp|avif|woff|woff2)$ {
    # none: 允许没有 Referer 头的请求(爬虫与直接输入地址)
    # blocked: 允许 Referer 头被防火墙或代理剥离后的请求
    valid_referers none blocked server_names *.example.com *.baidu.com *.google.com *.googlebot.com;

    if ($invalid_referer) {
        return 403;
    }
}

如果你希望彻底杜绝爬虫被防盗链误杀,还可以通过 Nginx 的 map 模块根据 User-Agent 提前把搜索引擎爬虫摘出来,直接跳过防盗链判定。


坑四:CDN 边缘节点缺少 Vary: Origin,缓存跨域污染

把 Nginx 配置改完后,我发现还有一个偶发的跨域失败现象。

因为静态资源前面挡了一层 CDN,当某一个静态文件(比如 app.css)在全网节点冷启动后,假如第一个访问这个文件的请求是普通请求(例如某台监控探针发出的没有带 Origin 头的纯 GET 请求),Nginx 返回了没有跨域头的普通内容。

CDN 边缘节点把这个响应缓存了下来。

随后,Googlebot 渲染器发起了带有 Origin: https://www.example.com 的跨域抓取请求,命中了 CDN 该边缘节点的缓存。CDN 没有区分来源,直接把刚才缓存的不带 Access-Control-Allow-Origin 的响应吐给了 Googlebot,再次触发 CORS 跨域报错。

在 HTTP 缓存规范中,如果一个资源的响应内容会随着请求头改变,服务端必须通过 Vary 头告知所有下游缓存中间件。

对于可能被跨域调用的静态文件,必须在响应头里明确加上 Vary: Origin:

add_header Vary "Origin, Accept-Encoding" always;

这样 CDN 节点在生成 Cache Key 时就会把 Origin 头纳入考量,请求带不同 Origin 或者不带 Origin 时,CDN 会分别缓存不同的响应副本,不会再互相污染。


生产环境 Nginx 完整配置模版

结合上述所有排查要点,给出一份跑在生产环境中的独立静态资源域名完整 Nginx 配置。这份配置涵盖了 MIME 类型、跨域全量输出、OPTIONS 预检快速响应、防盗链白名单以及 Vary 缓存隔离。

server {
    listen 443 ssl http2;
    server_name static.example.com;

    ssl_certificate /etc/nginx/ssl/static.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/static.example.com.key;

    root /data/www/static;
    index index.html;

    # 全局安全限制,禁止访问隐藏文件
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # 针对静态资源(CSS、JS、字体、图片)的核心处理规则
    location ~* \.(css|js|eot|ttf|woff|woff2|svg|png|jpg|jpeg|gif|webp|avif|ico)$ {
        # 1. 放行空 Referer 与主流搜索引擎
        valid_referers none blocked server_names *.example.com *.baidu.com *.google.com *.googlebot.com;
        if ($invalid_referer) {
            return 403;
        }

        # 2. 强制全状态码输出跨域允许头
        add_header Access-Control-Allow-Origin "*" always;
        add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
        add_header Access-Control-Allow-Headers "*" always;

        # 3. 告知 CDN 根据 Origin 与压缩算法区分缓存
        add_header Vary "Origin, Accept-Encoding" always;

        # 4. 快速处理 OPTIONS 预检请求
        if ($request_method = 'OPTIONS') {
            return 204;
        }

        # 5. 缓存周期与基础优化
        expires 365d;
        add_header Cache-Control "public, max-age=31536000, immutable" always;
        access_log off;
    }
}

上线后的三条验收命令

改动配置并重载 Nginx 后,不要只用自己的浏览器刷新看页面,必须在终端用 curl 把关键链路全部覆盖测试一遍:

第一条命令,验证主站跨域请求是否带出 CORS 头:

curl -I -H "Origin: https://www.example.com" https://static.example.com/assets/fonts/inter-latin.woff2

检查响应中是否存在 access-control-allow-origin: *。

第二条命令,验证协商缓存 304 下跨域头是否依然健在:

curl -I -H "Origin: https://www.example.com" \
     -H "If-Modified-Since: Mon, 13 Oct 2026 03:00:10 GMT" \
     https://static.example.com/assets/fonts/inter-latin.woff2

确认返回 HTTP/2 304 的同时,access-control-allow-origin: * 依旧存在。

第三条命令,模拟爬虫无 Referer 抓取 CSS:

curl -I -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
     https://static.example.com/assets/css/app.min.css

确认返回 HTTP 200,说明请求顺利通过,未被防盗链规则阻断。

三条命令全部通过后,再去 Google Search Console 的“网址检查”工具里点“测试实际网址”,在回传的屏幕截屏里就能看到完整的渲染版面。字体显示完备,CSS 布局恢复正常,因资源拦截导致的假性移动可用性报警也会在接下来的两个抓取周期内自动消除。

评论一下?

OωO
取消