CORS(Cross-origin resource sharing)的介绍

太长若不看,请看这里

  • 同源策略其实并不阻止其它域的请求, 而是使JavaScript不能获取到响应。
  • CORS设置头部可以得到跨域响应。
  • 与证书相关的CORS值得注意。

我仅仅在这里展示请求处理代码,整个例子可以在这里找到

我们一个例子开始看。假设我们有一个很棒的网站,为了保护我们的私人·数据,它拥有登陆功能,我们可以在 **/private ** 接口访问到私人数据:

app.get('/private', function(req, res) {
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET') 
  } else { 
    res.send('Please login first')
  }
})

为了不让这个例子变得很复杂,所以我们假设所有用户的密码都是:** secret** ,还有我们将使用cookie来保存我们的私人数据:

app.post('/login', function(req, res) { 
  if(req.body.password === 'secret') { 
    req.session.loggedIn = true 
    res.send('You are now logged in!') 
  } else { 
    res.send('Wrong password.') 
  }
})

我们的网站也提供公开接口,比如 ** /public **,用来访问公开数据:

app.get('/public', function(req, res) { 
  res.send('Public info') 
})

从其它域通过AJAX请求我们提供的API

现在虽然我们的API没有经过精心设计,但是我们至少能够从** /public 取到数据。
假设我们的API地址是 ** good.com/public ** , 客户端访问的域名是 ** thirdparty.com
, 客户端发起请求的代码如下:

fetch('http://good.com:3000/public')
  .then(response => response.text()) 
  .then((result) => { 
    document.body.textContent = result 
  })

这段代码没有达到预期效果!

我们可以通过开发者工具来看看 ** http://thirdparty.com ** 下的网络部分:

我们很容易看出虽然请求成功了,但是并没有拿到请求结果。可以从console部分找到原因:

原来如此,我们缺少了* Access-Control-Allow-Origin* 头部,但是为什么我们需要它?它又有什么优点?

同源策略

我们通过 JavaScript 无法获取到请求响应结果是由于同源策略的限制。此策略的目的是确保一个网站不能得到请求其它网站的响应结果。

例如,如果你访问一个网站 ** example.org **,你绝对不会同意这个网站
向你的银行网站发起请求并且拿到你的账户余额数据和交易数据。所以同源策略的意义就在这。

同源策略的“源”是由以下几部分组成:

  • 协议(例如: http)
  • 主机(例如: example.com)
  • 端口(例如: 8000)

所以 ** http://example.org **、 http://www.example.orghttps://example.org 不同源。

关于CSRF(Cross Site Request Forgery)的一点点知识

我们需要知道有一种叫做跨站请求伪造的攻击方式,它并不受同源策略的影响。

在一次跨站请求伪造攻击中,攻击者一般在背后向第三方网站发请求。例如可以在背后向你的银行网站发起POST方式请求,如果你在本地有银行网址有效的session,任何网站都可以在背后发起请求,除非你的银行有关于CSRF的对策。

我们还需要知道尽管同源策略是有效的,我们例子中从** thirdparty.com成功向 good.com **发起了请求,虽然没有得到响应结果,但是对CSRF攻击来说,并不需要得到响应结果。

让我们的API支持CORS

现在我们的目的是让第三方网站(例如:thirdparty.com)能够得到对我们API的请求结果,我们像错误提示那样设置CORS头部:

app.get('/public', function(req, res) { 
  res.set('Access-Control-Allow-Origin', '*') 
  res.send('Public info') 
})

我们设置头部“Access-Control-Allow-Origin”为“*”的目的是在浏览器中任何网站都能请求这个URL和拿到请求结果:


不简单的请求和预请求

前面的例子所谓的简单请求,是有着很少头部键值对的GET和POST请求。
我们现在改动一点点我们的API:

app.get('/public', function(req, res) { 
  res.set('Access-Control-Allow-Origin', '*')
  res.send(JSON.stringify({ 
    message: 'This is public info' 
  }))
})

同时 **thirdparty.com ** 客户端也稍微改变请求,如下:

fetch('http://good.com:3000/public', { 
  headers: { 
    'Content-Type': 'application/json' 
  } 
})
  .then(response => response.json())
  .then((result) => { 
    document.body.textContent = result.message 
})

这时我们可以从netwark 板块看到,还是拿不到请求结果:

请求方式不为GET和POST,还有请求Content-Type不为下面三种的任何请求都将拿不到请求结果。

  • ** text/plain **

  • ** application/x-www-form-urlencoded **

  • ** multipart/form-data **

其它Content-Type类型在跨域时都需要预先发起一个预请求。

这种机制的目的是让服务器决定是否允许浏览器发起真正的请求。浏览器设置请求头部 ** Access-Control-Request-Headers ** 和 ** Access-Control-Request-Method ** 后,服务器便能知道浏览器所希望返回的数据,同时服务器也需要返回响应请求头部字段。

我们现在还没有返回响应请求头部字段,所以需要增加这些:

app.get('/public', function(req, res) { 
  res.set('Access-Control-Allow-Origin', '*') 
  res.set('Access-Control-Allow-Methods', 'GET, OPTIONS') 
  res.set('Access-Control-Allow-Headers', 'Content-Type')
  res.send(JSON.stringify({ 
    message: 'This is public info' 
  }))
})

现在,** thirdparty.com ** 便能够获取到请求响应返回数据。

凭证和CORS

假设我们已经登录进了 ** good.com ** ,能够通过URL ** /private ** 能够获取到敏感信息。

假如已经把所有CORS设置已经设置好,像 ** evil.com ** 其它网站通过 ** /private ** 能够获取到敏感信息吗?

下面就让我们来看看:

fetch('http://good.com:3000/private')
  .then(response => response.text())
  .then((result) => { 
    let output = document.createElement('div')
    output.textContent = result 
    document.body.appendChild(output) 
  })

无论我们是否登录,都会看到 “Please login first” 的信息。

出现这种情况的原因是 good.com 的 cookie 不会被其它网址的请求所传输,本例中evil.com就是这种情况。

尽管是跨域,但是我们可以让浏览器发送cookie。

fetch('http://good.com:3000/private', { 
  credentials: 'include' 
}) 
  .then(response => response.text()) 
  .then((result) => { 
    let output = document.createElement('div')
    output.textContent = result 
    document.body.appendChild(output) 
  })

此时还是不起作用,不过,这也是一件好事。

想象一下,任何网站都可以向good.com发起认证请求,请求实际发生了但cookie并没有传输过去,请求响应结果也同样拿不到。

所以,我们不想让evil.com能够拿到我们的隐私数据,但是又想让thirdparty.com 能够访问 /private, 我们该如何做?

这种情况下我们应该把响应头部字段 ** Aceess-Control-Allow-Credentials ** 设置为 ** true **:

app.get('/private', function(req, res) { 
  res.set('Access-Control-Allow-Origin', '*')
  res.set('Access-Control-Allow-Credentials', 'true') 
  if(req.session.loggedIn === true) { 
    res.send('THIS IS THE SECRET') 
  } else { 
    res.send('Please login first') 
  }
})

但是这样还是不可行,** 允许所有域名都可以发起跨域认证请求是一个危险的动作 **。

浏览器不会轻易允许这种错误发生。

当我们想让 thirdparty.com 访问 /private 时, 我们可以在头部中指定:

app.get('/private', function(req, res) { 
  res.set('Access-Control-Allow-Origin', 'http://thirdparty.com:8000') 
  res.set('Access-Control-Allow-Credentials', 'true') 
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET') 
  } else {
    res.send('Please login first') 
  }
})

现在,http://thirdparty:8000 也可以获取到隐私数据了,但是 evil.com 依然不可以。

允许多域名访问

现在我们已经允许了一个域名能够发起跨域认证请求,我们如何更多域名呢?

这种情况下,我们可以想到使用白名单:

const ALLOWED_ORIGINS = [ 
  'http://anotherthirdparty.com:8000', 
  'http://thirdparty.com:8000' 
] 
app.get('/private', function(req, res) { 
  if(ALLOWED_ORIGINS.indexOf(req.headers.origin) > -1) { 
    res.set('Access-Control-Allow-Credentials', 'true')     
    res.set('Access-Control-Allow-Origin', req.headers.origin)
  } else { // allow others to make non-authed CORS requests
    res.set('Access-Control-Allow-Origin', '*') 
  } 
  if(req.session.loggedIn === true) { 
    res.send('THIS IS THE SECRET') 
  } else { 
    res.send('Please login first') 
  }
})

再次提醒: 不要设置 req.headers.origin 为 Access-Cotroll-Allow-Origin 的值,这样将会允许任何网站向的网站发起认证请求。

也许会有一些例外, 但是在没有白名单情况下实现带cookie的跨域资源共享(CORS)时谨慎考虑。

总结

在本篇文章中,我们回顾了同源策略和我们在需要时如何借助CORS实现跨域请求。
这需要服务端和客户端配合设置,一些基于这些设置的请求会出现有预请求的请求。
另外值得我们需要注意是,处理跨域认证请求时一个白名单能够保证多网站跨域请求而没有泄露敏感数据的风险。

译者注

本文翻译至这里,译者水平有限,错漏缺点在所难免,希望读者批评指正。另:欢迎大家留言讨论。

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

推荐阅读更多精彩内容