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

为了打通多业务线账号给全站接入了统一单点登录(SSO / OAuth2),搜索收录与关键词排名却断崖式暴跌?排查未登录爬虫全量遭遇 302 弹到登录页致快照沦陷、OAuth 回调带 state 动态参数炸出海量重复死链、爬虫无 Cookie 陷入授权重定向循环与 Nginx 白名单精准放行我踩过的几个深坑

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

文章讲述了作者在实施统一单点登录系统后,搜索收录和关键词排名急剧下降的问题。原因在于SSO系统导致未登录爬虫被重定向到登录页,形成软404错误和大量重复死链,同时OAuth登录按钮的动态state参数引发了百万级蜘蛛陷阱。作者建议调整SSO机制,确保公开内容正常返回200状态码,避免对搜索引擎产生负面影响。

上个月把博客、技术文档库和新建的开发者社区做了账号体系整合。之前三个站点各自独立维护一套用户表和 Session,不仅登录态无法跨站共享,后台统计也支离破碎。为了让读者在一个站点登录后在其他子站无需重复输入密码,我按照业界主流的 OAuth2 / OIDC 规范搭了一套统一单点登录系统(SSO)。

网关层接入很快,本地联调多域名单点登入与登出顺畅无阻,甚至还顺手加了跨域静默续期。周四下午灰度上线后,各业务线在桌面端和移动端的账号切换体验挑不出毛病。

结果到了下周一,例行查看站长工具时我整个人愣住了。百度搜索资源平台和 Google Search Console 上的收录曲线同时出现近乎垂直的跌幅,全站有效索引量几天内缩水了将近三分之二。此前几个带来稳定自然流量的长尾技术词,在搜索结果页里几乎绝迹。

在站长后台使用“URL 检查”调出各大搜索引擎近期抓取的渲染快照,屏幕上赫然出现的是统一认证中心的界面:

“统一认证中心 - 请输入账号与密码登录”

几百篇原本包含大段代码实现和排坑记录的技术文章,快照里的正文、代码高亮、内部锚文本和结构化数据全部消失,取而代之的是冰冷的登录输入框。Googlebot 直接对大批文章标注了“软 404:目标页面为登录页”并从索引库中剥离,百度蜘蛛抓取频次更是一路降到底谷。

一套初衷是优化用户体验的认证系统,直接在搜索引擎那里制造了一场灾难。排查这起事故的过程中,我把现代 SSO 机制与搜索引擎爬虫交互时暴露出来的隐蔽漏洞梳理了一遍。


鉴权中间件一刀切:把不带 Cookie 的爬虫全部 302 弹走

许多开发团队在设计认证网关时,思维惯性是“全站受保护”。

在 Nginx 网关层或者后端框架(如 Spring Security、NestJS Guards、Express 中间件)里,最常见的写法就是挂一个全局拦截逻辑:检查请求上下文。如果有合法的 Session Cookie 或者 Authorization 头,放行;如果没有任何凭证,直接返回 HTTP 302,重定向到认证中心的登录页:

HTTP/1.1 302 Found
Location: https://sso.llbbs.cn/oauth/authorize?client_id=blog&response_type=code&redirect_uri=https://www.llbbs.cn/oauth/callback
Cache-Control: no-cache

在普通访客的浏览器里,未登录时被弹去登录页再正常不过。

但搜索引擎蜘蛛(Baiduspider、Googlebot、Bingbot)的抓取逻辑完全不同。爬虫在发起 HTTP 请求时,绝不会主动携带任何历史 Session Cookie,也不会像人一样在页面上输入账号密码。

当爬虫抓取 https://www.llbbs.cn/article/1024.html 这篇公开文章时,网关检测到没有登录凭证,毫不犹豫地抛出了一个 302 重定向。爬虫顺着 Location 头跳转,最终到达 https://sso.llbbs.cn/oauth/authorize,拿到一个返回 200 状态码的登录表单 HTML。

这一连串交互在搜索引擎眼中会触发两种致命后果:

  1. 软 404 惩罚(Soft 404):Google 算法对“把内容 URL 重定向到登录页”的行为有极高敏感度。当一个原本返回大篇幅技术内容的页面突然 302 到包含用户名密码输入框的通用页面,算法会立即判定该内容已被删除或不可访问,将其归入 Soft 404 错误并快速降权清理。
  2. 快照被统一替换与内容合并:百度爬虫如果选择追踪 302 目标,会把目标页的文本和标题提取为快照。由于全站成百上千个技术页面的最终跳转结果都是同一个登录页,搜索引擎的去重算法直接将这些页面判定为高重复率镜像,导致主索引库直接剔除重复快照。

公开内容站点的基本原则是:读权限完全公开,写权限与个性化操作才需要受限。

文章正文、分类页、标签聚合页和站点地图必须无条件返回 HTTP 200 与完整 HTML。点赞、发表评论、查看个人中心等需要登录态的操作,应当在前端通过 JavaScript 异步交互或按钮点击时触发鉴权,绝不能在整页路由层对爬虫亮红灯。


OAuth 登录按钮生成动态 state,炸出百万级蜘蛛陷阱

当把全局 302 拦截撤掉、保证公开文章返回 200 后,我以为收录会逐步恢复。但两天后翻看 Nginx 访问日志,发现了更可怕的现象:爬虫并发数暴涨了十倍,全站抓取配额被迅速榨干,服务器的 CPU 使用率居高不下。

翻看日志里的抓取路径,全是一长串形态各异的 URL:

/oauth/authorize?client_id=blog&state=e8fbc102-7a4e-4b2c-9011-82d8c32f910a&redirect_uri=...
/oauth/authorize?client_id=blog&state=b17a9e03-3d12-4c55-88a2-710e9f1a040b&redirect_uri=...
/oauth/authorize?client_id=blog&state=4f923c88-12ab-4ef0-bb34-59a102c77132&redirect_uri=...

爬虫没有去抓取新发布的文章,反而把所有的算力耗在了这批动态链接上。

排查模板代码后,我找到了症结。在站点的顶部导航栏右侧,有一个“登录”按钮。为了图省事,服务端渲染(SSR)模板直接把认证链接写死在 <a> 标签里:

<!-- 致命写法:服务端直接在 HTML 标签中输出带动态随机防伪串的跳转链接 -->
<a href="https://sso.llbbs.cn/oauth/authorize?client_id=blog&response_type=code&scope=read&redirect_uri=https://www.llbbs.cn/oauth/callback&state=<?= generate_csrf_state() ?>">
  登录
</a>

根据 OAuth2 安全规范,为了防止跨站请求伪造(CSRF),每次发起授权时必须生成一个唯一的随机 state 字符串并存入服务端或 Session。

搜索引擎爬虫看到 <a> 标签后,会提取里面的 href 并加入待抓取队列。因为爬虫每次访问页面时拿到的是一次全新的服务端渲染,generate_csrf_state() 每次都生成一个全新的 UUID。

在爬虫眼中,每一个带不同 state 的 URL 都是一个完全独立的页面。

假设全站有 500 篇文章,页面被抓取一次就产生 500 个带不同 state 的 URL;爬虫顺着这些新链接抓过去,每个链接返回的页面里又包含整站内链,再次渲染出更多全新的 state。

这种指数级膨胀的 URL 黑洞在技术 SEO 里被称为蜘蛛陷阱(Spider Trap)。搜索引擎有限的抓取预算(Crawl Budget)全被消耗在无意义的认证跳转链接上,真正的核心文章根本排不上抓取队列。

对于登录跳转链接,正确的工程解法有两个:

第一,在 robots.txt 中严格封锁认证路由与回调路径:

User-agent: *
Disallow: /oauth/
Disallow: /api/auth/
Disallow: /login
Disallow: /*?*state=*
Disallow: /*?*code=*

第二,改造前端交互。登录入口不直接暴露带有一次性动态参数的直链,要么在 <a> 标签上打上 rel="nofollow",要么把登录动作封装成纯前端的按钮事件:

<!-- 安全写法:静态入口,点击时由前端即时获取授权地址,爬虫无需提取 -->
<button type="button" class="btn-login" onclick="handleLoginRedirect()">登录</button>

<script>
function handleLoginRedirect() {
  const state = crypto.randomUUID();
  sessionStorage.setItem('oauth_state', state);
  const target = 'https://sso.llbbs.cn/oauth/authorize?client_id=blog&response_type=code&redirect_uri=' 
    + encodeURIComponent(window.location.origin + '/oauth/callback') 
    + '&state=' + state;
  window.location.href = target;
}
</script>

爬虫无状态访问回调地址,引发 302 死循环风暴

除了授权入口被抓爆,回调路由 /oauth/callback 在日志里也留下了大量 500 和重定向死循环报错。

标准 Authorization Code 流程里,用户在 SSO 页面登录成功后,认证中心会携带着授权码 code 和防伪串 state 重新跳回业务站点的回调地址:

https://www.llbbs.cn/oauth/callback?code=AUTH_CODE_123&state=ORIGINAL_STATE

业务站点后端的处理流程通常是:

  1. 从 URL 取出 code;
  2. 用后端 HTTP Client 向认证中心换取 access_token 与用户资料;
  3. 将用户 ID 和登录凭证写入业务站点的 Session 或 Cookie;
  4. 返回 302,把用户送回原本浏览的页面(redirect_uri)。

当爬虫顺着页面内链误入了这个回调链接时,灾难发生了。

爬虫由于没有上下文,访问的 code 往往是几小时前已经被消费过一次的失效码,或者干脆是爬虫拼接出来的畸形参数。后端拿着失效的 code 去 SSO 换 token,SSO 接口返回 400 Bad Request,直接导致业务端回调抛出 500 内部服务器错误。

即便部分爬虫侥幸拿到了尚未过期的授权码完成换票,后端成功执行了 Set-Cookie: session_id=xxx 并返回 302 让客户端重定向回文章页。

但搜索引擎爬虫遵循无状态抓取规则,大部分爬虫在跟随 302 跳转时根本不会保留上一个响应头里的 Set-Cookie。

爬虫跳回文章页后,依然两手空空,没有任何凭证。如果此时文章页仍有局部未登录拦截逻辑,就又会把爬虫 302 弹去 SSO;SSO 又因为配置错误自动静默跳回 Callback,Callback 又让爬虫跳回文章页。

在连续跳转十余次后,爬虫沙箱触发了“Too Many Redirects(重定向过多)”的硬性风暴熔断,直接在抓取报告中对该 URL 标红报错。大量死循环错误集中爆发,会直接触发搜索引擎对源站稳定性的降权评级。

在应用层,回调路由 /oauth/callback 必须做到两点:

  1. 响应头显式返回 X-Robots-Tag: noindex, nofollow,直接从协议层命令爬虫不得索引该路径;
  2. 在接收到无效、过期或重复使用的 code 时,优雅返回明确的 400 或 403 页面,严禁通过重定向进行隐式重试。

Canonical 标签动态取 Host,把整站权重送给 SSO 子域名

排查过程中发现的第四个漏洞藏在 SSR 生成的页面头部。

为了防止同一篇文章因为带有查询参数(如渠道统计 ?from=feed、分页等)导致权重分散,现代网站通常会在 <head> 中嵌入标准 Canonical 规范化标签:

<link rel="canonical" href="https://www.llbbs.cn/article/1024.html" />

很多全栈框架或模板引擎在生成 Canonical 时,习惯从请求上下文动态获取域名:

// 常见隐患写法:动态拼接当前请求的 Host 和 Path
const canonicalUrl = `https://${req.headers.host}${req.baseUrl}${req.path}`;

在引入 SSO 架构后,为了支撑多站点登录态同步,有时会配置反向代理或者利用 SSO 域名作为某些静态资产的代理分流。

在某些特殊链路下,请求经过内网反代到达业务应用层,X-Forwarded-Host 或者反代透传的 Host 变成了 sso.llbbs.cn。由于后端代码没有做严格的主域名硬编码,SSR 渲染出来的 HTML 变成了:

<link rel="canonical" href="https://sso.llbbs.cn/article/1024.html" />

当爬虫抓取到这个标签时,搜索引擎接收到的信号是:“当前页面的规范主版本不在当前站点,而在 sso.llbbs.cn”。

结果就是,搜索引擎开始把原本属于内容站点的长尾词权重,全面尝试向认证中心子域名转移。而认证中心根路径根本不存在对应的文章内容,最终导致搜索引擎把两端都判定为无效页面。

Canonical 标签的使命就是消除不确定性。生产环境下必须写死站点固定的权威生产域名,并且在拼装 URL 时严格剥离一切无关的 Query 参数:

// 稳健写法:硬编码主站权威域名,剔除所有临时认证参数
function buildCanonicalUrl(pathname) {
  const PRIMARY_DOMAIN = 'https://www.llbbs.cn';
  const cleanPath = pathname.split('?')[0].replace(/\/+$/, '');
  return `${PRIMARY_DOMAIN}${cleanPath}.html`;
}

Nginx 网关动静分流与合规爬虫白名单放行配置

理清这四个问题后,整套修复方案的关键落在了 Nginx 网关层。核心逻辑包括三点:

  1. 公开只读路由与私有操作路由物理隔离:文章正文、分类、静态资源一律禁止接入前置 auth 子请求,直接透传后端渲染;
  2. 认证与回调路径从协议层阻断爬虫:所有 OAuth 相关路由强制输出 X-Robots-Tag;
  3. 针对合规爬虫实施双向 rDNS 反查验证:确保任何误判逻辑都不会伤及正牌蜘蛛。

下面是调整后的 Nginx 核心配置:

# 1. 识别主流搜索引擎爬虫的 User-Agent
map $http_user_agent $is_search_spider {
    default 0;
    ~*(Baiduspider|Googlebot|bingbot|Bytespider|360Spider|Sogou) 1;
}

# 2. 区分公开路径与私有路径
map $uri $is_public_content {
    default 0;
    ~*^/(article|post|archives|category|tag|feed|sitemap) 1;
    ~*^/(css|js|images|fonts|assets)/ 1;
    ~*^/(favicon\.ico|robots\.txt)$ 1;
}

server {
    listen 443 ssl http2;
    server_name www.llbbs.cn;

    # SSL 与安全头配置略...

    # 封锁 OAuth 回调与认证中间件路由,禁止任何爬虫索取
    location ~* ^/(oauth|auth|api/auth)/ {
        # 强制下发 noindex,防止搜索引擎意外收录
        add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
        add_header Cache-Control "no-store, no-cache, must-revalidate" always;

        proxy_pass http://backend_auth_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 公开内容路由:绝对严禁前置 auth_request,确保 100% 吐出 200 与正文
    location / {
        # 如果是公开文章或公共资源,直接放行给应用
        # 如果某些页面确实需要登录才能看(如会员专区),且不是合规蜘蛛,才允许 302
        if ($is_search_spider = 1) {
            # 真实合规蜘蛛绝对放行,绝不返回 302 登录页
            break;
        }

        # 正常转发应用服务
        proxy_pass http://node_ssr_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

配置上线后,可以用模拟爬虫 UA 的方式对页面发起针对性测试:

# 模拟百度蜘蛛请求文章页,验证返回码与内容
curl -I -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
  https://www.llbbs.cn/article/1024.html

终端返回的状态行应当是确凿的 HTTP/2 200,且响应头里绝不能包含指向登录页的 Location 或强制客户端重定向的 Set-Cookie:

HTTP/2 200
server: nginx
content-type: text/html; charset=utf-8
cache-control: public, max-age=600
x-frame-options: SAMEORIGIN

紧接着再核查 OAuth 回调入口的响应头:

curl -I https://www.llbbs.cn/oauth/callback?code=test

确认包含 x-robots-tag: noindex, nofollow, noarchive,彻底封死收录通道。

打通多站账号本身是工程演进中的常见需求,但如果把内网系统的强制鉴权习惯照搬到面向公开检索的内容站点上,爬虫会在几天之内用断崖式下跌的抓取数据敲响警钟。改完这一整套网关隔离与爬虫放行策略后,我在站长后台提交了全站死链清理与快照重审,熬了大概两周,收录曲线和关键词排名才逐步回升到原有水平。以后只要调整涉及全站全局的网关与认证中间件,上线前第一项必做的事项就是拿无 Cookie 的爬虫 UA 先去把全站公开页面通跑一遍。

评论一下?

OωO
取消