震惊!由 Referer 引发的一桩“惨案”?
案件起因
震惊!全员震惊😱!一位程序员哈某举报有人偷走了他两天两夜的时间,经过我们24小时的跟踪暗访,嫌疑人竟是 Mr.Referrer ?请看详细报道:
前端同事的一个 jsp 资源A,通过浏览器去访问第三方业务的某个资源B,资源B会做一个 login 认证请求,如果 login 成功则返回资源B的详细页面信息,反之进入 ErrorPage。在测试环境中,login 是可以成功的;但是在生产环境中,login 请求提示了如下错误:
Provisional headers are shown.
直译是:显示了临时的(暂定的)标题。经抓包查看网络状况是:HTTP Code 200 OK,但是 Response 是空的,更像是浏览器主动取消了这个请求,Google 和 Safari 都是一样的现象。
哈某排查了许久也没有定位到原因,面临客户提出的紧急上线,情况很是焦灼。
FYI: 不想听我讲废话的话,请直接跳转至 Referer 😂
分析过程
1.谷歌一下
相信大部分程序员遇到问题时,第一个排查步骤都是施展 CV 大法粘到谷歌搜索一下。同样,我们和哈某也采取了这一步,搜索 Provisional headers are shown
得到的结果都大同小异。以下几个原因粘贴自这里
具体原因有多种总结如下:
1.请求被某些扩展如 Adblock 拦截了,请求被浏览器插件拦截。解决方案:用 chrome://net-internals 来帮助你查找被屏蔽的请求以及可能的原因。(本人发现现在这个用不了了);
2.请求被墙了;
3.走本地缓存或者 dataurl 的请求。强缓存 from disk cache 或者 from memory cache,此时也不会显示;
4.服务器未及时响应(超时);
5.跨域,请求被浏览器拦截;
6.其他原因;
这里的原因6,我们就当他在钻研废话文学吧……总而言之,网友们提供的这几个原因,经尝试,依旧没有定位到问题。
2.检查 Nginx 配置
经过上面“专业”的排查~依旧懵逼。随后哈某联系了服务端的同事,想比对一下测试环境、生产环境中 Nginx 的配置是否有区别,燃鹅并没有区别。为了避开 Nginx 存在的未知干扰,从内网访问资源B,login 的操作显示 404 🙄……
二度懵逼~😳
3.沟通
通过专业手段没有定位到原因,只能尝试和第三方业务的人沟通了。按照我们大部门的管理理念,没有什么事情是沟通解决不了的。
很不幸的是,对端的人只反馈给我们 login 请求并没有到达他们那儿。听到这个回答笑哭了有没有~用哈某的话说:“我都快看他内裤了,还不承认”,今天凌晨躺在床上帮他们分析这个问题时(其实我不懂后端,就是跟着瞎掺和顺便学习新知识),被这句话逗笑了,也算是通宵加班的一个慰藉了。😂
4.Charles
通过对 App 抓包,login 请求同样显示 HTTP Code 200 OK,但是 Response 是空的。但是跟 Google 浏览器中 NetWork 调试器的表现不太一样。
5.cURL
为了排除浏览器对 Response 可能存在特殊的解析规则,亦或者服务器针对浏览器发出的请求做一些限制,可以通过 cURL 作网络请求。cURL入门学习:阮一峰-curl 的用法指南 。
通过 cURL 访问资源B以及 login 请求,Ummmmmmmm,请求是成功的!经过一位”神棍”的提点,我们从 Google 浏览器中选中目标请求 -> 右击 -> Copy -> Copy as cURL:
比对一下和自己使用 cURL 写的请求区别在哪里,结果如下:
页面关系
1
2
31、先访问资源A: https://hhh.com:0000/aaa/test.jsp
2、由1跳转到资源B: https://hhh.com:0000/aaa/index.html
3、由2发起 login 认证请求 https://hhh.com:0000/aaa/loginAction.doGoogle 发出的请求(失败的情况)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16curl 'https://hhh.com:0000/aaa/loginAction.do' \
-H 'Connection: keep-alive' \
-H 'Pragma: no-cache' \
-H 'Cache-Control: no-cache' \
-H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"' \
-H 'Accept: application/json, text/plain, */*' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Referer: https://hhh.com:0000/aaa/index.html' \
-H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' \
-H 'Cookie: JSESSIONID=68ED14BCB51A01E5DA08FA2009333BA2' \
--compressed -i自己发的 cURL 请求(成功的情况)
1
curl 'https://hhh.com:0000/aaa/loginAction.do' -i
所以区别在哪里呢?header !
6.破案
经过修改几个主要的 header 信息,最终确定了真凶就是 Referer
字段。去掉 Referer
之后页面可以正常访问了。无奈由于资源B是三方系统,我们无法要求对方修改代码,采用了临时的解决方案:
修改 DMZ 区 Nginx 的配置,所有经过这台 Nginx 的请求都不携带 Referer 头部。
1 | add_header Referrer-Policy no-referrer; |
所以 Provisional headers are shown
翻译为”临时的头部信息“更为贴切对吗?不知道是否因为 Referer 涉及用户隐私的问题。
Referrer or Referer ?
Referer
是 HTTP 请求头里一个常见的字段,包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer
请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
1.Referer 的含义
现实生活中,你一定填过这样的问卷调查:”你从哪里知道了我们?”。这叫做引荐人(referrer),谁引荐了你?对于公司来说,这是很有用的信息。互联网也是一样,你不会无缘无故访问一个网页,总是有人告诉你,可以去那里看看。服务器也想知道,你的 “引荐人” 是谁?
HTTP 协议在请求(request)的头信息里面,设计了一个 Referer
字段,给出 “引荐网页” 的 URL。这个字段是可选的。客户端发送请求的时候,自主决定是否加上该字段。
很有趣的是,这个字段的拼写是错的。Referer
的正确拼写是 Referrer
,但是写入标准的时候,不知为何,没人发现少了一个字母r
。标准定案以后,只能将错就错,所有头信息的该字段都一律错误拼写成 Referer
。Nginx 采用的是正确拼写~
2.Referer 的发生场景
浏览器向服务器请求资源的时候,Referer
字段的逻辑是这样的,用户在地址栏输入网址,或者选中浏览器书签,就不发送 Referer
字段。
主要是以下三种场景,会发送 Referer
字段:
(1)用户点击网页上的链接。
(2)用户发送表单。
(3)网页加载静态资源,比如加载图片、脚本、样式。
1 | <!-- 加载图片 --> |
上面这些场景,浏览器都会将当前网址作为 Referer
字段,放在 HTTP 请求的头信息发送。
浏览器的 JavaScript 引擎提供 document.referrer
属性,可以查看当前页面的引荐来源。注意,这里采用的是正确拼写。
3.Referer 的作用
Referer
字段实际上告诉了服务器,用户在访问当前资源之前的位置。这往往可以用来用户跟踪。一个典型的应用是,有些网站不允许图片外链,只有自家的网站才能显示图片,外部网站加载图片就会报错。它的实现就是基于 Referer
字段,如果该字段的网址是自家网址,就放行。
由于涉及隐私,很多时候不适合发送 Referer
字段。这里举两个例子,都不适合暴露 URL。一个是功能 URL,即有的 URL 不要登录,可以访问,就能直接完成密码重置、邮件退订等功能。另一个是内网 URL,不希望外部用户知道内网有这样的地址。Referer
字段很可能把这些 URL 暴露出去。
此外,还有一种特殊情况,需要定制 Referer
字段。比如社交网站上,用户在对话中提到某个网址。这时,不希望暴露用户所在的原始网址,但是可以暴露社交网站的域名,让对方知道,是我贡献了你的流量。
4.rel
属性
由于上一节的原因,浏览器提供一系列手段,允许改变默认的 Referer
行为。
对于开发者来说,rel="noreferrer"
属性是最简单的一种方法。<a>
、<area>
和 <form>
三个标签可以使用这个属性,一旦使用,该元素就不会发送Referer
字段。
1 | <a href="..." rel="noreferrer" target="_blank">xxx</a> |
上面链接点击产生的 HTTP 请求,不会带有 Referer
字段。
注意,rel="noreferrer"
采用的是正确的拼写。
5.Referrer Policy 的值
rel
属性只能定制单个元素的 Referer
行为,而且选择比较少,只能发送或不发送。W3C 为此制定了更强大的 Referrer Policy。
Referrer Policy 可以设定 8 个值:
(1)no-referrer
不发送 Referer
字段。
(2)no-referrer-when-downgrade
如果从 HTTPS 网址链接到 HTTP 网址,不发送 Referer
字段,其他情况发送(包括 HTTP 网址链接到 HTTP 网址)。这是浏览器的默认行为。
(3)same-origin
链接到同源网址(协议 + 域名 + 端口 都相同)时发送,否则不发送。注意,https://foo.com
链接到 http://foo.com
也属于跨域。
(4)origin
Referer
字段一律只发送源信息(协议 + 域名 + 端口),不管是否跨域。
(5)strict-origin
如果从 HTTPS 网址链接到 HTTP 网址,不发送 Referer
字段,其他情况只发送源信息。
(6)origin-when-cross-origin
同源时,发送完整的 Referer
字段,跨域时发送源信息。
(7)strict-origin-when-cross-origin
同源时,发送完整的 Referer
字段;跨域时,如果 HTTPS 网址链接到 HTTP 网址,不发送 Referer
字段,否则发送源信息。
(8)unsafe-url
Referer
字段包含源信息、路径和查询字符串,不包含锚点、用户名和密码。
6.Referrer Policy 的用法
Referrer Policy 有多种使用方法。
(1)HTTP 头信息
服务器发送网页的时候,通过 HTTP 头信息的 Referrer-Policy
告诉浏览器。
1 | Referrer-Policy: origin |
(2)<meta>
标签
也可以使用 <meta>
标签,在网页头部设置。
1 | <meta name="referrer" content="origin"> |
(3)referrerpolicy
属性
<a>
、<area>
、<img>
、<iframe>
和 <link>
标签,可以设置 referrerpolicy 属性。
1 | <a href="..." referrerpolicy="origin" target="_blank">xxx</a> |
疑惑
资源B的测试环境同样有 Referer 头部,但所有网络请求都是没问题的。虽然这次通过修改 Nginx 的配置临时解决了问题,但生产环境上为何存在这个问题,根本原因依旧没搞清楚。同事也咨询了对方是否针对 Referer 做了类似防盗链的处理,得到的答案是否定的。所以可能是哪里的问题呢?😳