301重定向那条链,三个地方各写了一份规则

301重定向那条链,三个地方各写了一份规则
张文保 更新 27 分钟阅读 3,016 阅读
本文目录
  1. 从四个门进同一个首页,要走几步?
  2. 四个入口会不会落到不同的终点
  3. 一条链上混着301和302,是谁写的?
  4. 那几跳到底是谁答的?
  5. 为什么这件事必然被写在三个地方?
  6. HSTS这第二份副本,说的是同一件事吗?
  7. 裸域和www,为什么会有两份不一样的HSTS?
  8. 那个91天的数字,是谁替你填的?
  9. 把HSTS写在http响应上,浏览器会读吗?
  10. 第三份副本存在浏览器里,谁在维护它?
  11. 这些多出来的跳,SEO上到底掉什么?
  12. 第一笔:抓取预算
  13. 第二笔:链条越长,信号越糊
  14. 第三笔:用临时码做永久归一
  15. 第四笔:HSTS那份声明的实际收益是安全,不是排名
  16. 我的尺子错在哪几处
  17. 自己站上怎么查、按什么顺序改
  18. 常见问题解答
  19. 首页跳两次,会不会影响排名?
  20. http到https那一跳能不能靠HSTS省掉?
  21. max-age该填多少?
  22. 裸域和www的HSTS必须一样吗?
  23. 怎么知道自己的域名在不在浏览器的预置清单里?
  24. preload进去了还能退出来吗?
  25. 这些数据能代表中文站吗?
  26. 权威参考资料
摘要:把131个海外品牌站的首页,分别从http裸域、http带www、https裸域、https带www四个门各进了一次。121个站能走到底,其中从http裸域进来要跳两次的59个、跳三次的17个、跳四次的还有2个。真正值得看的不是跳了几次,是26个站在同一条链上混用了两种以上的跳转码——301和302并排出现,一个说永久一个说临时;还有19个站,能从响应头直接认出相邻两跳是由不同的层作答的。再往下,同一件事还有第二份和第三份副本:HSTS响应头109个站发了,可裸域和www两台主机给出的不是同一份的有12个;而浏览器内置的那份预置清单里,8个站在头里写着preload、清单里却查不到,其中一个的状态是被拒。

这件事是从一个很小的疑问开始的。有天保哥在给一个客户看抓取日志,发现Googlebot请求首页的次数不少,但真正拿到200的比例低得有点古怪。顺手用curl把首页的四种写法各敲了一遍,才看清楚:从最朴素的那个写法进去,要连跳三次才落地。

更有意思的是,那三跳的状态码不一样。第一跳301,第二跳301,第三跳302。同一个动作——把你送到唯一那个首页——被写成了三条规则,而且第三条用的还是临时跳转。

一条规则写错不奇怪。三条规则写在三个不同的地方,只有一条被改过,这才是常态。这轮就是冲着这个去的:把131个站的首页当成一个测量对象,看同一件事在几个地方各存了一份,以及这几份彼此对不对得上。

从四个门进同一个首页,要走几步?

先说样本。131个站里,121个能从至少一个入口走到200;剩下10个对我这个客户端一律403或418(aboutyou、boohoo、farfetch、fjallraven、madewell、nespresso、pandora、thefarmersdog、underarmour、warbyparker),拿不到就是拿不到,不硬凑。

四个入口分别是:http加裸域、http加www、https加裸域、https加www。每一条都不跟随重定向,一跳一跳地记状态码和响应头。

入口写法0跳直达跳1次跳2次跳3次跳4次
http://裸域/04159172
http://www./1644582
https://裸域/34641930
https://www./6051820

规范终点落在www上的81个,落在裸域上的40个。这个分布本身没什么好争的,两种选法在排名上没有差别,纠结它属于浪费时间,具体理由www和非www哪个更好这个老问题里已经算过账。

值得停一下的是最后一列。跳三次、跳四次的那批不是小站:theordinary从http带www进来要连跳四次才到家,suitsupply和cybex-online同样是四跳,byredo是301、302、308三种码连着来。这类链条不是设计出来的,是长出来的。

四个入口会不会落到不同的终点

这是我开工时最想抓的一条:如果几层规则各写各的,四个入口应该有机会落到不同的地址上。

结果是0例外。121个站,四个基础入口最终都落在同一个200地址上,一个反例都没有。这条假设被数据直接否掉了,我把它写在这里而不是删掉,是因为它给后面所有结论定了一个底:这些站在“终点是谁”这件事上是有共识的,出问题的从来不是终点,是通往终点的路径。尺子能量出反例,只是这次反例的数量恰好是零,这跟尺子没量对是两回事。

一条链上混着301和302,是谁写的?

HTTP语义的现行规范把这几个码分得很清楚:301是永久,302是临时。永久意味着“以后别再来问了,直接去新地址”,临时意味着“这次先这么走,下次还得再问我一遍”。把域名归一这种十年不变的事写成302,等于每一次抓取都要多花一个来回。

我统计的是:同一条链上,出现了两种或两种以上的3xx状态码。

121个站里有26个中招,占21.5%。按入口算是68条链。组合分布很集中:

状态码组合链条数这两个码分别在说什么
301+30254一条说永久搬走了,一条说这次临时借道
301+3074永久搬走,和“临时且不许改方法”
301+302+3083三种语义挤在一条链上
302+3083临时和“永久且不许改方法”
其余组合4301+307+308、307+308、302+307各若干

arcteryx那条特别典型:从http带www进去,依次是301、308、307。三跳三个码。308和307是301和302的“不许把POST改成GET”版本,一条链上同时出现,基本可以断定不是同一个人在同一个地方写的。

我本来打算把“归一路径上用了临时码”单独统计一遍,结果发现这批站和上面那26个完全重合,一个不多一个不少。反过来说就是:没有任何一条链是把301和308这两个永久码混着用的,凡是混用,混进来的那一节必定是临时跳转。

这个重合不是巧合,它把机制说得比我原来想说的还清楚:后加的那一层,倾向于用临时码。临时码在心理上是“我先这么应付一下,回头再改”,可回头这件事从来不发生,于是这句“先这么走”就在链条里待了下来。301、302、404和410各自的语义边界是十几年没变过的东西,但它变不变,和有没有人回头去改那行配置,是两码事。

那几跳到底是谁答的?

光看状态码只能说“像是两个人写的”。要坐实,得看每一跳是谁答的。

办法很土但有效:把每一跳响应里的serverviacf-rayx-vercel-idx-amz-cf-idx-azure-ref这类能认出厂商的头都记下来,然后看同一条链上相邻两跳的标识变没变。

19个站的链条上,相邻两跳来自不同的层。点几个名字:

  • braun.com:从http裸域进来,第一跳由Azure的应用网关答,第二跳由IIS答,最后落在Azure的CDN上。三跳,三层。
  • canyon.com:第一跳的server写着某域名商的托管服务,第二跳写着同一家的转发服务,第三跳才是Cloudflare。域名商那边留着的转发规则,和CDN上配的规则,谁也不知道谁。
  • bonprix.com:Apache答两跳,然后交给CloudFront。
  • dji.com:CloudFront先答一个301,再由另一层答302。
  • charleskeith.com:一个叫urllo的转发服务先答一跳,之后两跳都在Cloudflare里绕。

这些痕迹做不了假。一个站要是所有跳转都在同一个nginx里做完,响应头会长得一模一样。头不一样,就说明这条链是拼出来的——每一段是不同时期、不同的人、在不同的控制台里加的。

为什么这件事必然被写在三个地方?

说到这里可以把机制摊开了。一个电商站上线到今天,“把访客送到唯一那个首页”这条规则,天然有至少三个可以写它的位置:

  1. 域名商或DNS那一层。买域名时顺手开的转发、别名,或者早年为了做跳转买的URL转发服务。这一层的特点是:开的时候是运营或者老板开的,之后谁都想不起来它还在。
  2. CDN或边缘那一层。Cloudflare的页面规则、CloudFront的函数、Akamai的属性配置。这一层改起来最快,所以出事时第一个被改的往往是它。
  3. 源站或应用那一层。nginx的server块、Apache的.htaccess、框架自己的中间件。这一层最“正统”,也最少被动,一份从旧配置里导进来的重写规则能在里面躺好几年。

三层都能把请求扔向同一个地址。问题是没有任何机制强制它们保持一致:你在CDN上加了一条规则解决问题,源站那条旧规则不会因此消失,它只是排到了后面,从“第一跳”变成了“第二跳”。链条就是这么一节一节长出来的,很像老房子里的电线:新拉一路不等于旧的那路被拆了,它只是被压在了墙里面。

这也是为什么改配置的时候看不出来。Nginx每次reload都通过,可Googlebot每8次抓取仍有1次撞在301上说的是同一件事的另一面:单层的语法检查永远是绿的,因为每一层单独看都没错。多层之间谁压过谁,也不一定按你以为的来——响应头和meta打架时赢的从来不是更严的那条,跳转这边的优先级同样是先到先得,不是更严格的先得。

顺带说一句边界。首页到底有几个写法能直接打开(多一道斜杠、加个index.html、把路径改成大写),这轮我也顺手量了,但那是另一个题目,同一个页面十一种URL写法都返回200已经把它拆得很细,这里不重复。本文只盯跳转链和跟着它的那几份声明。

HSTS这第二份副本,说的是同一件事吗?

http到https那一跳,除了301之外还有第二种说法,叫HSTS。它是一个响应头,意思是“从今天起的这么长时间里,浏览器你别再用http来找我了,自己改成https”。

换句话说,301规则和HSTS头在说同一件事:这个站只走https。只不过一个是每次现场拦,一个是提前打招呼、让浏览器自己在本地拦。

121个站里,规范终点上带HSTS的有109个。听着不错,拆开看就没那么好了:

max-age档位站数说明
不到30天1基本等于没开
30天到180天53最大的一档
180天到1年7
整1年(31536000)39规范推荐的下限
超过1年9两年居多

另外两个附加项更能说明问题:写了includeSubDomains的只有23个,写了preload标记的只有10个。也就是说,绝大多数站的这份声明只覆盖它自己这一台主机。

裸域和www,为什么会有两份不一样的HSTS?

这是本轮最直接命中主题的一处。同一个域名,裸域和www是两台主机,各自发各自的响应头。理论上,同一件事应该说得一模一样。

这一节的口径和上一节不同:上一节看的是规范终点那一台,这一节把裸域和www分开各看一次。两边都发且完全一致的99个,两边都发但不一样的9个,只有一边发的3个,两边都不发的10个。那12个对不上的:

站点裸域那份www那份
bollandbranchmax-age=7889238max-age=31536000
burrowmax-age=31536000max-age=7889238
chubbiesshortsmax-age=7889238max-age=31536000
gymsharkmax-age=7889238max-age=31536000; includeSubDomains; preload
harrysmax-age=7889238max-age=31536000
italicmax-age=7889238max-age=31536000
nomadgoodsmax-age=31536000max-age=7889238
suitsupplymax-age=63072000max-age=86400 ; includeSubDomains
prosemax-age=31536000, max-age=31536000max-age=31536000
bonprixmax-age=604800不发
peakdesign不发max-age=31536000
zwillingmax-age=63072000; includeSubDomains不发

suitsupply那一对差得最狠:一边两年,一边一天,中间隔着730倍。而prose那一行更值得看——它的裸域响应里,HSTS头出现了两次。两层各自加了一遍,谁也没发现另一层已经加过了。值一样纯属运气好,要是不一样,浏览器会按第一个出现的算,后面那个白写。这个坑和响应头自相矛盾那一轮里量到的形态是同一类。

还有一个细节:这12个对不上的里面,有6个的差异形态一模一样——一边是7889238,一边是31536000。这个规律太整齐,整齐到不像是巧合。

那个91天的数字,是谁替你填的?

7889238秒是多久?91.3天。不是整月,不是整季度,不是整年,是一个谁也不会手打出来的数。

它在121个站里出现了58次。一个值出现58次,只有两种解释:要么58家公司的运维碰巧都喜欢这个数,要么它根本不是这58家填的。

要坐实第二种,光看这55个站不够——那只是在找支持自己的证据。按自基线的做法,得把没发这个值的站也一起量一遍,看这个值到底跟着什么走。于是我把能走到200的118个站的首页HTML全拉了一遍,只认页面里的资源域名这种硬指纹,不猜。这一步的样本比上面少3个,是因为有3个站这轮HTML没拿回来,下面的百分比都按118这个分母算。

结果分成两组:发7889238的55个站,指纹全部指向Shopify,占100%;不发这个值的63个站里,认不出平台的26个、Shopify 12个、Salesforce 10个、Next.js 9个、Magento 3个、BigCommerce 2个、WordPress 1个。

方向非常清楚:发这个值的,无一例外都在同一个平台上;这个值就是平台的出厂值,没有任何一个站主碰过它。

反过来那一栏更有意思。同一个平台上,有12个站没用出厂值——说明这个值是能改的。可这12个站改出来的结果是:

  • 改成整1年(31536000)的6个:anker、eufy、govee、peakdesign、soundcore、vuoriclothing
  • 改成两年(63072000)的3个:buckmason、kotn、ruggable
  • 改成180天(15552000)的2个:mejuri、shopify自己
  • 干脆不发的1个:joolz

这就是那句话最好的注脚:没人管的时候,55个站说的是同一句话;一旦有人管,12个站管出了4种不同的说法。统一从来不是维护出来的,是没人动出来的。这和白名单在没人复查时慢慢漂移是一个机制的两面。

把HSTS写在http响应上,浏览器会读吗?

不会。HSTS的规范原文写得很明白:这个头只有出现在https响应上才作数,出现在http响应上必须被忽略。道理也简单——http连接本来就可能被中间人改,如果允许它宣布“以后都走https”,那反过来也就允许中间人宣布别的。

但有8个站在http的301响应里也发了HSTS:arcteryx、assos、lookfantastic、philips、rab.equipment、rituals、sostrenegrene、vaude。

这行头不会造成任何危害,它只是被安静地丢掉。危害在于它会让人误判:你在浏览器的网络面板里看到这行头,会以为HSTS已经生效了。它是一句说给听不见的人听的话。

这类“写了但没人读”的形态,在响应头里那248个死字段那一轮里已经量过一次,只不过那次是字段本身没人认,这次是位置不对。

第三份副本存在浏览器里,谁在维护它?

HSTS有个先天缺口:浏览器第一次访问这个站之前,根本没收到过那个头,所以第一次请求还是会走http。补这个缺口的办法叫preload——把域名提交到一份清单里,这份清单直接编译进浏览器,浏览器出厂时就知道这个站只走https。

这就是第三份副本。它和前两份的区别在于:前两份在你的服务器上,改了立刻生效;第三份被直接编译进浏览器,进去慢,出来更慢。

我拿域名逐个问了这份清单的状态接口,131个站的答案相当冷清:已经在清单里的3个,进去过又被移出的3个,申请过被拒的1个,剩下124个从来没在这份清单上出现过。

然后是这轮最锋利的一组对照。头里明明白白写着preload标记、清单里却查不到的,有8个站:

站点它自己发的HSTS头清单里的真实状态
bellroymax-age=15552000; preload从未收录
bromptonmax-age=31536000; includeSubDomains; preload从未收录
gymsharkmax-age=31536000; includeSubDomains; preload从未收录
lookfantasticmax-age=31536000; includeSubDomains; preload从未收录
quincemax-age=31536000; includeSubDomains; preload从未收录
ritualsmax-age=63072000; includeSubDomains; preload从未收录
byredomax-age=31536000; includeSubDomains; preload已被移出
shopifymax-age=15552000; includeSubDomains; preload被拒

preload那个标记,本身没有任何功能。它不是开关,是一句声明:“我同意被收进清单”。真正决定你在不在清单里的,是清单那边的审核。这8个站的头里写着“我在”,清单说“你不在”。

最后两行值得单独讲。byredo的状态是已被移出——它进去过,后来因为某次不满足条件被拿了出来,可站上那行preload标记留到了今天。而shopify自己那一行,是这轮最完整的一个闭环:清单的硬条件之一是max-age至少一年,它写的是15552000,180天,只有一半。带着一个不满足条件的值去申请,结果当然是被拒;被拒之后,那个preload标记还在头里挂着。

一家给几十万商家提供建站服务的公司,自己的域名在这份清单上的状态是rejected,而它替客户填的那个默认max-age是91天——离清单要求的门槛更远。这不是嘲笑谁,这恰恰说明问题不在能力,在于没有任何一个环节负责把这三份副本拉回同一个值上。

还有一个反方向的例子:aboutyou在清单里的状态是preloaded,而它对我这个客户端一律返回403,那个403响应里一个HSTS头都没有。清单说它承诺只走https,现场拿不到任何承诺。这个观察有边界——403是给我这种客户端的,真实浏览器看到的可能不一样,所以我只把它当成一个提醒,不当结论。

这些多出来的跳,SEO上到底掉什么?

把上面的东西落到能算账的地方。判断标准不用自己发明,搜索侧对各类跳转的处理方式是公开写着的。

第一笔:抓取预算

一次多余的跳转,对爬虫就是一次多余的往返。首页是被抓得最频繁的地址,一天几十上百次很常见。多跳一次,一年就是几万次白跑。这笔账在大站上是实打实的,小站上感受不明显,但也没有任何理由留着。条件请求那一轮算过同类的账,形态不同,逻辑一样。

第二笔:链条越长,信号越糊

关于301是否损耗权重,那个流传多年的说法早就该退休了,15%损耗的来历我单独写过。但“不损耗”不等于“随便跳”。链条长了会带来两个实际后果:一是超过一定跳数搜索引擎会放弃跟随;二是链条中间任何一跳出问题,整条路径断掉,而你从终点做体检永远看不见。

第三笔:用临时码做永久归一

这是26个站的实际状态。302告诉爬虫“原地址还有效,下次还来问”,于是原地址不会被合并掉,两个地址长期并存在索引里,谁被选成规范页由算法决定,不由你决定。canonical这件事你说了不算那一轮已经量过一次自我声明和实际判定的落差,跳转码是同一个故事的上游。

第四笔:HSTS那份声明的实际收益是安全,不是排名

得说清楚:HSTS不是排名因素。它值得做,是因为它把“第一次访问走明文”这个窗口关掉了,也顺带把http到https那一跳从每次请求省成一次。做它是为了安全和速度,不是为了排名,别在方案里把它包装成SEO项目。

我的尺子错在哪几处

四次,其中两次改变了结论。

错在哪差点写出的错结论怎么发现的
并发太高,34个站返回429把“我被限流了”读成“这个站没数据”,样本直接缺掉四分之一429集中出现在同一批站上,而这批站单独慢速重跑就正常。限流不是站点的属性,是我自己的属性
拿响应头认建站平台想用x-shopid证明那55个站是同一个平台,全样本命中0,差点写成“认不出平台”回头看真实响应头,那批站全部走同一家CDN,平台标识被挡在后面。改用页面里的资源域名才量得出来
把“没抓到”当成“没有指纹”归因台第一版有18个站抓取失败,被算进了“没有平台指纹”那一栏,100%变成了67%失败的那18个恰好是我手工验过确实在那个平台上的,数字和手感对不上
只统计发了那个出厂值的站“这个值跟着平台走”会变成一句没有对照组的话补量了不发这个值的63个站,才知道同平台里还有12个改过

第二和第三条是同一个毛病的两种形态:用一把从没验证过能不能量出反例的尺子,去量一个自己已经有预期的对象。第一条则是老问题了,连着几轮都栽在同一处。

自己站上怎么查、按什么顺序改

不需要任何工具,curl就够。

for u in "http://例子.com/" "http://www.例子.com/" \
         "https://例子.com/" "https://www.例子.com/"; do
  echo "== $u"
  curl -sSI -o /dev/null -w '%{http_code} -> %{redirect_url}\n' "$u"
done

要看完整链条和每一跳是谁答的,把-I换成带-D -的GET,逐跳读serverviacf-ray这几个头。注意用HEAD查有可能查不全,有些站对HEAD和GET给的头不一样。

改的顺序,按投入产出排:

  1. 先把链条压到最多一跳。目标是任何一个入口最多跳一次就到终点。做法是在最外层(通常是CDN)一次性判完协议和主机名,直接给出最终地址,而不是让它一层层往里传。
  2. 把归一路径上的临时码全换成301。如果链路上有POST请求需要保留方法,用308,别用302。
  3. 去把源站和域名商那两层的老规则找出来。这一步最花时间,也最容易被跳过。判据是:把CDN规则临时关掉,看请求会落到哪儿——落到哪儿,哪儿就还留着一条你忘了的规则。这一步别在业务高峰做。手上有多个域名要合并的,顺序上先把域名整合那套决策走完再来清链条,否则清完还得再清一遍。
  4. 裸域和www两台主机的HSTS对齐。两边发同一个值,或者明确只让其中一台对外。各类服务器上这个头怎么配、出问题怎么回滚,HSTS的开启与回滚流程那篇写得比这里细。
  5. 决定要不要preload,然后让三份副本说同一句话。不打算提交清单,就把头里那个preload标记删掉——留着它只会让下一个接手的人以为已经做过了。打算提交,就先把max-age提到一年以上、补上includeSubDomains,再去提交,并且记住这件事是单向门:进清单容易,退出来要等好几个浏览器版本。

最后一句提醒,来自那8个站:一行声明写在自己家里,不等于外面那份记录跟着变了。凡是需要第三方点头的声明,都要在自己这边留一个能查回去的动作,否则它就会像那8个preload标记一样,一直挂在那里说着一句不再成立的话。这和证书有效期一路砍到47天之后续期只能自动化是同一个道理:凡是有有效期又不由你单方面决定的东西,都得有人盯。

这一轮和前两轮其实是一条线上的。政策页那行最后更新日期量的是人写给人看的声明没人再动,Cookie到期日那一轮量的是同一个到期时刻有三种续期方式,这一轮量的则是同一件事被三个地方各写了一份。共同的那句话是:写下来那天都是对的,之后没人负责让它们一起变。

常见问题解答

首页跳两次,会不会影响排名?

直接影响排名的证据没有。实际影响是抓取效率和链路可靠性:多一跳就多一次往返,链路中间任何一节坏掉,从终点做体检都发现不了。真要排优先级,它排在内容和索引问题后面,但属于一次性改完就长期受益的那类,性价比很高。

http到https那一跳能不能靠HSTS省掉?

只能省掉回头客那一次。浏览器第一次访问你的站之前没收到过这个头,第一跳还是走http。所以那条301规则不能删,HSTS是加在它旁边的,不是替代它。只有进了浏览器的预置清单,第一次访问才会被直接改写。

max-age该填多少?

不打算提交预置清单,一年(31536000)是个稳妥值。刚开始上,可以先填一个小值观察几天,确认全站包括所有子域都能走https之后再提上去。要提交清单,一年是硬门槛,还得同时带上includeSubDomains和preload两个标记,三者缺一不可。

裸域和www的HSTS必须一样吗?

规范没有强制,但没有理由不一样。它们是同一个品牌的同一个站,给浏览器两个不同的期限只会让行为难以预测。如果其中一台只用来跳转,最省事的做法是让它发和主站完全相同的那一份。

怎么知道自己的域名在不在浏览器的预置清单里?

清单方提供了公开的查询页,输入域名就能看到状态:从未收录、待收录、已收录、待移除、已移除。别用“我头里写了preload”当判据,那只是一句申请意向,不是收录结果。

preload进去了还能退出来吗?

能,但很慢。提交移除申请之后,还要等浏览器发布新版本、用户升级到那个版本,旧版本浏览器在缓存过期前仍然只走https。所以提交前要确认所有子域,包括那些内部用的、老的、快忘了的,全都支持https。这是一扇很难往回走的门。

这些数据能代表中文站吗?

不能直接代表。样本全是海外品牌的独立站,CDN和建站平台的构成跟国内很不一样。但“同一件事被几层各写一份、只有一层被改过”这个机制跟地域无关,做法照搬即可,数字别照搬。

权威参考资料

分享到
标签
版权声明

本文标题:《301重定向那条链,三个地方各写了一份规则》

本文链接:https://zhangwenbao.com/redirect-chain-three-layers-hsts-preload-audit.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交