二狗子翻车了,只因上了这个网站……

今天故事的主角还是大家熟识的二狗子。二狗子拿到了一笔项目奖金,在好好犒劳了自己一顿后,决定把剩下的钱在银行存个定期。

他用浏览器访问了 www.bank.com,输入了用户名和密码后,成功登录。

bank.com 返回了 cookie 用来标识二狗子这个用户。

不得不说,浏览器是个认真负责的工具,它会把这个 cookie 记录下来,以后二狗子每次向 bank.com 发起 HTTP 请求,浏览器都会准确无误地把 cookie 加入到 HTTP 请求头部中,一起发送到 bank.com,这样 bank.com 就知道二狗子已经登陆过了,就可以按照二狗子的请求来做事情,比如查看余额、转账取钱。

二狗子存完钱,看着账户余额,心中暗喜。于是,他打开了 www.meinv.com,去看自己喜欢的电影。

但二狗子不知道的是,浏览器把 meinv.com 的 HTML、JavaScript 都下载到本地,开始执行。而其中某个 JavaScript 中,偷偷创建了一个 XMLHttpRequest 对象,然后向 bank.com 发起了 HTTP 请求 。

浏览器严格按照规定,把之前存储的 cookie 也添加到 HTTP 请求中。但是 bank.com 根本不知道这个 HTTP 请求是 meinv.com 的 JavaScript 发出的,还以为是二狗子发出的。bank.com 检查了cookie,发现这是一个登录过的用户,于是兢兢业业地去执行请求命令,二狗子的个人信息就泄露了。(ps. 实际中实施这样一次攻击不会这么简单,银行网站肯定是做了其他很多安全校验的措施,本故事只是用来说明基本原理。)

可怜的二狗子还不知道发生了什么,已经遭受了钱财损失。那我们来帮他复盘一下为什么会发生这种情况。

首先,每当访问 bank.com 的时候,不管是人点击按钮访问链接,还是通过程序的方式,存储在浏览器的 bank.com 的 cookie 都会进行传递。

其次,从 meinv.com 下载的 JavaScript 利用 XMLHttp 访问了 bank.com。

第一点我们是无法阻止的,如果阻止了,cookie 就丧失了它的主要作用。

对于第二点,浏览器必须做出限制,不能让来自 meinv.com 的 JavaScript 去访问 bank.com。这个限制就是同源策略。

同源策略

浏览器提供了 fetch API 或 XMLHttpRequest 等方式,它们可以使我们方便快捷地向后端发起请求,取得资源,展示在前端上。而通过 fetch API 或 XMLHttpRequest 等方式发起的 HTTP 请求,就必须要遵守同源策略 。
那什么是同源策略呢?同源策略(same-origin policy)规定了当浏览器使用 JavaScript 发起 HTTP 请求时,如果是请求域名同源的情況下,请求不会受到限制。但如果是非同源的请求,则会强制遵守 CORS (Cross-Origin Resource Sharing,跨源资源共享) 的规范,否则浏览器就会将请求拦截。

那什么情况下是同源呢?同源策略非常严格,要求两个 URL 必须满足下面三个条件才算同源:

1、协议(http/https)相同;

2、域名(domain)相同;

3、端口(port)相同。

举个例子:下列哪些 URL 地址与 https://www.bank.com/withdraw.html 属于同源?

因此,当我们请求不同源的 URL 地址时,就会产生一个跨域 HTTP 请求(cross-origin http request)。

例如想要在 https://www.upyun.com 的页面上显示来自 https://opentalk.upyun.com 的资讯内容,我们使用浏览器提供的 fetch API 来发起一个请求:

···
try {
fetch('https://opentalk.upyun.com/data')
} catch (err) {
console.error(err);
}
···

这就产生了一个跨域请求,跨域请求则必须遵守 CORS 的规范。

当请求的服务器没有配置允许 CORS 访问或者不允许来源地址的话,请求就会失败,在 Chrome 的开发者工具台上就会看到以下的经典错误:

···
Access to fetch at 'https://opentalk.upyun.com/data' from origin 'https://www.upyun.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

···

那在实际应用中,我们该如何正确地设定 CORS 呢?

什么是 CORS

CORS 是针对不同源(域)的请求而制定的规范。浏览器在请求不同域的资源时,被跨域请求的服务端必须明确地告知浏览器其允许何种请求。只有在服务器允许范围内的请求才能够被浏览器放行并请求,否则会被浏览器拦截,访问失败。

在 CORS 规范中,跨域请求主要分为两种:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求

简单请求必须符合以下四个条件,实际开发中我们一般只关注前面两个条件:

(1)使用 GET、POST、HEAD 其中一种方法;

(2)只使用了如下的安全请求头部,不得人为设置其他请求头部:

  • Accept

  • Accept-Language

  • Content-Language

Content-Type 仅限以下三种:

  • text/plain

  • multipart/form-data

  • application/x-www-form-urlencoded

(3)请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器,XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问;

(4)请求中没有使用 ReadableStream 对象。

不符合以上任一条件的请求就是非简单请求。浏览器对于简单请求和非简单请求,处理的方式也不一样。

对于简单请求,浏览器会直接发出 CORS 请求。具体来说,就是在请求头信息中,自动地增加一个Origin (来源)字段。

Origin 的值中,包含请求协议、域名和端口三个部分,用于说明本次请求来自哪个源。服务器可以根据这个值,决定是否同意这次请求。例如下面的请求头报文:

···
GET /data HTTP/2
Host: opentalk.upyun.com
accept-encoding: deflate, gzip
accept: /
origin: https://www.upyun.com
......
···

如果 Origin 指定的源不在服务器允许范围内,服务器会返回响应一个正常的 HTTP,浏览器发现回应头部中,如果没有包含 Access-Control-Allow-Origin 字段,就会抛出错误。需要注意的是,这种错误无法通过状态码识别,HTTP 响应的状态码有可能是 200。

如果 Origin 指定的源在允许范围内的话,响应头部中,就会有以下几个字段:

···
Access-Control-Allow-Origin: https://www.upyun.com
Access-Control-Allow-Headers: Authorization
Access-Control-Expose-Headers: X-Date
Access-Control-Allow-Credentials: true
···

大家可能也看出来了一个特点,与 CORS 请求相关的字段,都以 Access-Control- 开头。

如果跨域请求是被允许的,那么响应头部中是必须有 Access-Control-Allow-Origin 头部的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意域名的请求。

Access-Control-Allow-Credentials 是一个可选字段,它的值是一个布尔值,表示是否允许发送Cookie。如果发起跨域请求时,设置了 withCredentials 标志为 true,浏览器在发起跨域请求时,也会同时向服务器发送 cookie。如果服务器端的响应中不存在 Access-Control-Allow-Credentials 头部,浏览器就不会响应内容。

特别需要说明的是,如果请求端设置了 withCredentials ,Access-Control-Allow-Origin 的值就必须是具体的域名值,而不能设置为 *,否则浏览器也会抛出跨域错误。

Access-Control-Expose-Headers 也是一个可选头部。当进行跨域请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本响应字段:

  • Cache-Control

  • Content-Language

  • Content-Type

  • Expires

  • Last-Modified

  • Pragma

而如果开发者需要获取其他响应头部字段,或者一些自定义响应头部,服务器就可以通过设置 Access-Control-Expose-Headers 头部来指定发起端可访问的响应头部。

非简单请求

非简单请求往往是对服务器有特殊要求的请求,比如请求方法为 PUT 或 DELETE,或者 Content-Type 字段类型是 application/json。

对于非简单请求的 CORS 请求,浏览器会在正式发起跨域请求之前,增加一次 HTTP 查询请求,我们称为预检请求(preflight)。浏览器会先询问服务器,当前的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 请求方法和请求头部字段。只有得到肯定答复,浏览器才会发出正式的跨域请求,否则就会报错。

比方说我们使用代码发起一个跨域请求:

···
fetch('http://opentalk.upyun.com/data/', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CUSTOM-HEADER': '123'
}
})
···

浏览器会发现这是一个非简单请求,它会自动发送一个 OPTIONS 的预检请求,其中核心内容有两部分,Access-Control-Request-Method 表示后面的跨域请求需要用到的方法,Access-Control-Request-Headers 表示后面的跨域请求头内会有该内容。

···
OPTIONS /data/ HTTP/1.1
Host: opentalk.upyun.com
Origin: http://www.upyun.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-MY-CUSTOM-HEADER, Content-Type
···

服务器收到预检请求后,检查这些特殊的请求方法和头自己能否接受,如果接受,会在响应头部中包含如下信息:

···
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: X-Date, range, X-Custom-Header, Content-Type
Access-Control-Expose-Headers: X-Date, X-File, Content-type
......
···

上面的 HTTP 响应中,关键的是 Access-Control-Allow-Origin 字段,* 表示同意任意跨源请求都可以请求数据。部分字段我们在简单请求中解释过了,这里挑几个需要注意的头部解释一下。

  • Access-Control-Allow-Methods,这是个不可缺少的字段,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。

  • Access-Control-Allow-Headers 字段为一个逗号分隔的字符串,表明服务器支持的所有请求头部信息字段,不限于浏览器在预检中请求的字段。

  • Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86400 秒),在此期间,不用再发出另一条预检请求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 79. http 响应码 301 和 302 代表的是什么?有什么区别? 答:301,302都是HTTP状态的编码...
    C乖阅读 1,574评论 0 0
  • 转自阮一峰的网络日志 CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource...
    zfylin阅读 99评论 0 1
  • CSRF & CORS 下面转的两篇文章分别说明了以下两个概念和一些解决方法: 1. CSRF - Cross-S...
    Elvis_zhou阅读 696评论 0 4
  • 1.发现问题 在开发基于WEB应用的时候,一般情况下前端负责界面展示,后端负责业务逻辑,尤其是当前分布式、微服务、...
    Integer泰哥阅读 1,080评论 0 0
  • 浏览器有一个重要的安全策略,就是 同源策略,它用于限制不同源之间资源的交互。能够帮助阻挡一些恶意的访问,减少可能被...
    海人为记阅读 231评论 0 0