Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

掌握并理解 CORS (跨域资源共享) #157

Open
husky-dot opened this issue Dec 3, 2019 · 0 comments
Open

掌握并理解 CORS (跨域资源共享) #157

husky-dot opened this issue Dec 3, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

husky-dot commented Dec 3, 2019

作者:Martin Splitt
译者:前端小智
来源:dev


阿里云双12已开启,新老用户均可参与,2核1G云服务器仅需79元,,更多服务器配置及价格请关注:Hi拼团,或点此了解“云上爆款1折特惠活动”。同时,建议在购买阿里云相关产品前先领取阿里云2000元代金券会更优惠哦


知识要点

  • 浏览器强制执行同源策略,拒绝不同站点的网站访问。
  • 同源策略不会阻止对其他源的请求,但是会禁用对 JS 响应的访问。
  • CORS 标头允许访问跨域响应。
  • CORS 与 Credentials 一起时需要谨慎。
  • CORS 是一个浏览器强制策略,其他应用程序不受此影响。

事例讲解

为了缩小代码量,这里演示部分代码,完全的代码在 Github 上可以得到。

咱们从一个例子开始,假设咱们有一个网站,网址为 http://good.com:8000/public:

app.get('/public', function(req, res) {
  res.send(JSON.stringify({
    message: 'This is public'
  }));
})

咱们还有一个简单的登录功能,用户可以输入一个共享的密匙并设置一个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.')
  }
})

咱们通过 /private获取一些私有数据,就可以通过上面登录状态来做进一步验证。

app.get('/private', function(req, res) {
  if(req.session.loggedIn === true) {
    res.send(JSON.stringify({
      message: 'THIS IS PRIVATE'
    }))
  } else {
    res.send(JSON.stringify({
      message: 'Please login first'
    }))
  }
})

通过 AJAX 从其他域请求咱们的 API

目前,咱们 API 并不是专门设计,但可以允许其他人从 /public URL 中获取数据。 假设咱们的API位于good.com:300/public上,并且咱们的客户端托管在thirdparty.com上,该客户端可能会运行以下代码:

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

但这在我们的浏览器中不起作用,通过控制的 network 来看看http://thirdparty.com 的请求:

请求成功,但结果不可用。原因可以在控制台找到:

啊哈!咱们缺少Access-Control-Allow-Origin标头。 但是,为什么我们需要它,它有什么用呢?

同源策略

我们在 JS 中得不到响应结果的原因是同源策略。该策略的目的是确保一个网站不能读取对另一个网站的请求的结果,并由浏览器强制执行。出于安全方面的考虑,现在的网页都用cookie来进行身份验证,如果不限制读取,网页B里的恶意脚本代码可以随意模仿真实用户进行操作。

例如: 如果在咱们�在 example.org上,并不会希望该网站向我们的银行网站发出请求,获取咱们的帐户余额和交易。

同源策略可以防止这种情况的发生。

在这种情况下,“来源”由

  • 协议(如http)
  • 域名(如 example.com)
  • 端口(如8000)

关于 CSRF(跨站点请求伪造) 的说明

请注意,有一类攻击称为CSRF(跨站点请求伪造),它无法通过同源策略来避免。

CSRF攻击中,攻击者向后台的第三方页面发出请求,例如向咱们的银行网站发送POST请求。如果我们与我们的银行存在一个有效的会话,任何网站都可以在后台发出请求,该请求将被执行,除非咱们的银行网站有针对CSRF的反措施。

注意,尽管同源策略已经生效,但是的咱们的示例请求从thirdparty.com成功请求到good.com,只是我们无法获得结果。但对于CSRF来说,不需要获取的结果。

例如,有个 API 通过POST请求方式发送邮件,返回的内容是咱们需要关心的,蛤攻击者不在乎结果,他们关心的是电子邮件是否有发送了成功。

为咱们的 API 启用 CORS

现在,咱们希望允许第三方站点(如thirdparty.com)上的 JS 访问咱们的 API 能得到响应。为此,我们可以根据错误提示启用CORS标头:

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

这里将access-control-allow-origin标头设置为*,这意味着:允许任何主机访问此URL和获取响应的结果:

非简单的请求和预检

如果请求不是简单请求,浏览器会先发送一个预请求:

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

前面的例子是一个的简单请求。简单的请求是带有一些允许的标头和标志头值的GETPOST请求。现在,对 thirdparty.com 进行了一些更改让它能获取到JSON格式的数据。

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

但这又让thirdparty.com崩溃了,network面板向我们展示了原因:

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的,头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1) Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是GET

(2) Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段.

此机制允许web服务器决定是否允许实际请求。浏览器设置Access-Control-Request-HeadersAccess-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可以再次获得响应。

凭证(credentials)和 CORS

现在,假设咱们已登录good.com并可以使用敏感信息访问 /private URL。通过设置CORS,可以让其他网站,比如evil.com获得这些敏感信息,来看看:

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

无论是否已经登录到good.com,都会看到“Please login first”。

原因是当请求来自另一个来源时,来自good.comcookie将不会被发送,在本例中为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)
  })

但同样,这无法在浏览器中工作,其实,这也是个好事。

象一下,任何网站都可以发出经过身份验证的请求,但不会发送实际的cookie,并且无法获得响应。

因此,咱们不希望evil.com能够访问此私有数据-但是,如果我们希望thirdparty.com可以访问/ private,该怎么办?

在这种情况下,需要将Access-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 other origins to make unauthenticated CORS requests
    res.set('Access-Control-Allow-Origin', '*')        
  }

  // let caches know that the response depends on the origin
  res.set('Vary', 'Origin');

  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

再次提醒:不要直接发送req.headers.origin作为CORS原始标头。这将允许任何网站访问对咱们的网站进行身份验证的请求。

这条规则可能有例外,但是在使用没有白名单的凭证实现CORS之前至少要三思。

总结

在本文中,咱们研究了同源策略以及如何在需要时使用CORS来允许跨源请求。

这需要服务器和客户端设置,并且根据请求会出现预检请求。

处理经过身份验证的跨域请求时,应格外小心。 白名单可以帮助允许多个来源,而不会冒泄露敏感数据(在身份验证后受到保护)的风险。

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/g33konaut/understanding-cors-aaf


交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant