货都到了,海关却不放行
服务器把数据箱送到了门口,JavaScript 伸手去接,浏览器海关却“啪”地落下闸门:“来源不同,先看通关许可!”货都运回来了,怎么还算失败?
箱子就在眼前
本文承接上一篇:《URL 第一次出门问路》。
“放下那箱数据!”
返程通道的警报一响,JavaScript 老大停在了半空。
准确地说,是一只手停在半空。另一只手还稳稳端着咖啡。
通道里,一只刚从服务器回来的数据箱正冒着热气。浏览器海关挡在箱子前,闸门落得又快又响,差点夹住 JavaScript 老大的袖口。
“服务器给我的!” 老大指着箱子。
“知道。”
“地址也是我填的!”
“看见了。”
“HTTP 刚才还说服务器回了 200!”
“挺顺利。”
JavaScript 老大把咖啡放下:“那为什么不让我拿?”
海关翻开来源登记表:“因为发请求的页面住在这里:”
https://app.example.com
“数据箱从这里回来:”
https://api.example.com
“不是一处。”
“都叫 example.com,怎么就不是一家?”
“大衣颜色一样,就能共用门禁卡?”
“不能。”
“那就别攀亲戚。”
三样对上才算自己人
浏览器海关把两张地址卡推进检查机。
机器“咔哒”一声,吐出三枚印章:
协议 主机 端口
“判断两个地址是不是同一个源,主要核对这三样。” 海关先敲协议,“http 和 https 不同源。”
又敲主机:“app.example.com 和 api.example.com 不同源。”
最后敲端口:“前两样相同,端口不同,照样不同源。”
JavaScript 老大拿出两张新卡:
https://app.example.com/news
https://app.example.com/settings
“路径不一样。”
“路径不参加这轮认亲。” 海关盖下“同源”章,“协议、主机、端口相同,就属于同一个源。”
老大又写了一张:
https://app.example.com:443/profile
海关瞄了一眼:“https 的默认端口就是 443。没写出来,不等于没有。”
“这规矩谁定的?”
“同源策略。”
海关指向身后的数据仓库。里面放着登录信息、私密页面和各种接口响应。
“要是任何网页里的脚本都能随便读取别处的数据,刚打开一个陌生页面,网上邻居就可能跑去翻其他站点的抽屉。”
JavaScript 老大往仓库看了一眼:“所以先把不同源都当外人。”
“先隔开。真要合作,再办手续。”
请求都发了,还算失败?
JavaScript 老大绕着数据箱走了一圈。
“有件事还是说不通。既然不同源,为什么不在出发时拦住?现在服务器已经收过请求,连响应都送回来了。”
海关拍拍箱盖:“谁说同源策略会把所有跨源请求都锁死在家里?”
“不然呢?”
“网页平时照样能加载许多来自别处的图片、样式和脚本。至于 fetch 这类由脚本发起的跨源读取,要按 CORS 规则检查响应能不能交给脚本。”
“CORS?”
“跨源资源共享。说白了,就是服务器给海关的放行说明。”
海关调出刚才的运输记录:
fetch("https://api.example.com/profile")
.then((response) => response.json())
.then((profile) => console.log(profile));
“这趟请求已经发出。浏览器还在请求里带了一张来源卡:”
Origin: https://app.example.com
服务器返回数据时,却没有说明是否允许这个来源读取。
JavaScript 老大拍了拍箱子:“所以东西到了,海关也看过了,但最后不给我?”
“对。在脚本眼里,这次 fetch 会失败,响应内容拿不到。”
“那服务器已经做过的事呢?”
“照常算数。海关拦的是响应,可不会跑去服务器把事情撤销。”
老大的手又停在了半空。
“所以不能拿 CORS 当服务器的防盗门?”
“总算问到重点了!” 海关在箱子上贴了一张醒目的便签,“CORS 主要限制脚本读取响应,不保证请求一定没有到达服务器。涉及改资料、扣库存这类操作,服务器仍然要自己检查身份、权限和请求是否合法。”
“箱子看得见,内容拿不到,事还可能已经办了。”
“这回记得很顺口。”
这次,许可跟着箱子来了
HTTP 抱着文件夹从服务器方向跑来。
“服务器那边把通关文件配好了!”
JavaScript 老大指着被扣下的数据箱:“贴到这只上面?”
海关一把按住箱盖:“想什么呢?这份响应回来时没有许可,不能事后贴张纸就重新开放。再发一次请求!”
JavaScript 老大只好重新填写工作单。
没过多久,一只新的数据箱回到返程通道。这一次,响应单上多了一行:
Access-Control-Allow-Origin: https://app.example.com
“这是什么意思?” JavaScript 老大探头。
“服务器明确表示:允许来自 https://app.example.com 的页面读取这份响应。”
海关把响应里的许可和请求里的 Origin 对了对。
完全一致。
闸门升起。
JavaScript 老大终于抱到数据箱,差点当场亲它一口。
“等等!” HTTP 往后退了两步,“亲箱子不在运输协议里。”
“高兴一下。”
“服务器要是愿意对不带凭证的任意来源开放公共数据,也可以这样写:”
Access-Control-Allow-Origin: *
JavaScript 老大眼睛一亮:“万能通行证!”
海关立刻把星号卡收走:“只是允许任意来源读取适合公开共享的响应。不是给所有请求发万能钥匙,更不能见接口就贴。”
“这个许可要写在请求里,还是响应里?”
“响应里。” HTTP 把文件夹抱紧,“允许谁读取,由提供资源的服务器表态。前端往请求里手写一个 Access-Control-Allow-Origin,相当于访客自己给自己盖章。”
“那我盖圆一点呢?”
“圆一点也是假章。”
普通件直接查,特殊件先敲门
新的数据箱刚放行,出发通道又响了。
这次准备出发的是一辆手续格外多的货车。
车头挂着 PUT,车厢贴着 Content-Type: application/json,顶部还竖着一面 Authorization 小旗。
海关远远看见,立刻按下红色按钮。
“先别发正件,派预检员!”
一名瘦小的 OPTIONS 从侧门钻了出去。
JavaScript 老大看愣了:“刚才的 GET 直接去了,这次怎么先派个空手的?”
“有些跨源请求方法和请求头比较简单,符合 CORS 安全清单,可以直接发送,再检查响应许可。” 海关指着货车,“这辆不在简单那一档。正式出发前,浏览器得先问服务器:这个来源想用 PUT,还想带这些请求头,行不行?”
OPTIONS 抵达服务器,递出一张询问单:
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
服务器没有收正式货物,只回了一份预检答复:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: authorization, content-type
海关逐项核对。
“来源,允许!”
“方法,允许!”
“请求头,也允许!”
红灯变绿,货车这才载着真正的请求出发。
JavaScript 老大戳了戳刚回来的 OPTIONS:“所以你只是探路,不负责把正式数据送过去。”
“对。” OPTIONS 擦擦汗,“预检通过,正式请求才出门;预检失败,正件留在原地。”
“每次都要跑两趟?”
“服务器可以用 Access-Control-Max-Age 告诉浏览器,这份预检结果允许缓存一段时间。” 海关给预检员递了杯水,“但浏览器会有自己的上限和缓存规则,不能拿它当永久免检证。”
OPTIONS 一口喝完:“谢谢。再跑几趟,我就要申请改名叫 EXHAUSTED 了。”
带着 Cookie,星号就失灵
JavaScript 老大正准备收起通关单,门外又挤进来一群小圆饼。
每只都背着行李,有的还系着安全带。
“我们也要跟请求走!”
海关脸色一变:“谁把 Cookie 放进来的?”
“下一篇的队伍提前排到了这里。” HTTP 说。
JavaScript 老大举起工作单:
fetch("https://api.example.com/account", {
credentials: "include"
});
“我要跨源请求时带上凭证。服务器已经写了星号,可以走吧?”
“不可以!” 海关把 * 从通行证上划掉,“请求带凭证时,Access-Control-Allow-Origin 不能用星号糊弄,服务器必须明确写出获准的来源。”
HTTP 赶紧重开一张响应单:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
“一张说明允许哪个源,一张说明允许带凭证的响应交给脚本。” 海关检查完,仍然没有立刻开门。
JavaScript 老大急了:“又缺什么?”
海关低头看向 Cookie 队伍:“CORS 许可齐了,不代表每只 Cookie 都能跟着走。它们还得看自己的 Domain、Path、Secure 和 SameSite 等行李规则。”
排在最前面的 Cookie 把帽檐往下一压:“这部分归下一班检查员。”
“还挺会留悬念。”
“系列文章都写到第十八篇了,多少学会一点。”
海关不是修路队
闸门前终于安静下来。
JavaScript 老大把今天的通关单摊在桌上:
“协议、主机、端口都相同,算同源。”
海关点头。
“不同源的脚本想读取响应,要看服务器给不给 CORS 许可。”
海关继续点头。
“符合安全清单的跨源请求可以先发,再查响应;不符合的,浏览器先用 OPTIONS 预检。”
OPTIONS 在旁边有气无力地挥了挥手。
“带凭证时,服务器要明确写允许的来源,还要给出 Access-Control-Allow-Credentials: true,不能只甩一个星号。”
Cookie 队伍齐刷刷抬头。
“最后,CORS 该由服务端正确配置。浏览器海关只按双方文件办事,不负责替服务器签字。”
海关满意地收起印章:“可以下班了!”
JavaScript 老大抱着数据箱往执行大厅走,走到一半又回头:“开发时把浏览器的安全检查关掉,算解决跨域吗?”
海关沉默了两秒。
“这不叫解决跨域,叫嫌检查麻烦,直接把海关拆了。”
“听起来不太适合上线。”
“也不适合拿来证明配置没问题。”
JavaScript 老大迅速划掉这个方案。
这时,Cookie 队伍已经在另一条通道排成了长龙。
检查员拿起扩音器:“去 api.example.com 的站左边,只匹配 /account 的往前一步,没系 Secure 安全带的先别挤!”
队尾一只 Cookie 踮起脚尖:“都是 Cookie,为什么有的能上车,有的不能?”
海关把扩音器递给下一班检查员。
“问得好!开始查行李。”