最难不过二叉树

跨域问题

2024-08-14

跨域问题在Web开发过程中很常见,如果浏览器没有同源策略来约束跨域请求,那么整个web安全将得不到保证。本文从跨域请求的安全性分析、浏览器同源策略、CORS处理跨域问题三个方面来谈谈web开发中的跨域问题。

跨域是什么

需要明确一点,跨域问题针对浏览器的,这是浏览器施加的安全限制。浏览器由于同源策略限制,当请求的资源与页面的协议、域名或端口任一不匹配时,浏览器会阻止该请求。

这里提到了同源策略,这里理解一下什么叫做“源”(Origin)?

源其实就是我们熟悉的url概念,源由协议、域名、端口 三部分组成。
如:https://blog.moonlet.cn (协议https, 域名blog.moonlet.cn,默认端口443)

image (26).png

当一个请求url的协议域名端口三者之间的任意一个与当前页面url不同即为跨域

同源策略保护了什么

浏览器的这种同源策略限制主要包含以下几点:

  • Cookie、LocalStorage和IndexDB无法读取非同源的资源。
  • DOM和JS对象无法获得非同源资源。例如iframe、img等标签加载的资源,DOM无法访问;JS无法操作非同源页面的DOM。
  • AJAX请求不能发送到非同源的域名,浏览器会阻止非同源的AJAX请求。
  • 不能读取非同源网页的Cookie、LocalStorage和IndexDB。

这里可以讨论一下,假设浏览器没有同源策略,我们进行跨域访问时,为什么会不安全。

这里举一个场景:

  1. 用户浏览正常网站bank.com
  2. 用户在bank.com上登录了账号
  3. 登录成功,用户浏览器上的Cookie保存了用户在bank.com的登录态信息
  4. 期间用户访问了freemovie.com这个网站想看电影,但这个网站是恶意网站
  5. freemovie.com有个图片,吸引了用户去点击,但是这个图片内嵌了恶意脚本代码,点击时触发请求http://bank.com/modify_password?new_password=123345
  6. 因为该请求带上了用户在bank.com的登录态信息,所以bank.com服务器认为该请求合法,因此bank.com服务器执行了该请求,在用户不知情的情况下,以用户的名义修改了用户密码

在这个案例中,用户的信息至此至终没有传输到freemovie.com恶意网站的后台,用户的登录态信息只有bank.com和用户浏览器之间传递,并没有信息泄露给第三方。但从结果来看,在用户信息没有泄露的情况下,用户的密码被黑客所修改了。这个过程就是大名鼎鼎的CSRF。

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

那问题出现在哪一步呢?

其实第5步是有问题的,问题不在bank.com服务器,因为请求带的是正确的登录态信息,那么就可以认为是用户本人,从逻辑上看是没问题。那问题更多出在了浏览器本身:为什么在用户不知情的情况下,发送了http://bank.com/modify_password?new_password=123345 的请求?在用户的角度,我在浏览freemovie.com时,在没有允许的情况下,对bank.com发起http请求,本身就是不合理的,我在A网站的各种操作,怎么会影响到B网站呢?

在这个场景里,我们看到了跨域带来的风险,所以同源策略规定了“AJAX请求不能发送到非同源的域名,浏览器会阻止非同源的AJAX请求”。在同源策略的规定下,场景1的第5步会被浏览器所禁止,请求发不出去。

问题来了,并不是所有的跨域http请求都是恶意请求,如果是都是一刀切浏览器AJAX请求不能发送到非同源的域名,那么一定会误伤无辜。那么怎么处理正常的跨域请求呢?

一个思路就是,浏览器在freemovie.com发起http://bank.com/modify_password?new_password=123345 前,先发个请求问问bank.com服务器,freemovie.com能不能信任。bank.com服务器回复浏览器,该网站不能信任,那么浏览器就直接禁止http://bank.com/modify_password?new_password=123345 发送。如果bank.com服务器回复浏览器,可以信任freemovie.com,那么浏览器就可以发送http://bank.com/modify_password?new_password=123345

以上就是CORS解决跨域问题的核心思路了,那这里详细介绍CORS。

CORS处理跨域问题

CORS,全称Cross-Origin Resource Sharing,浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。浏览器对这两种请求的处理,是不一样的。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin表示本次请求来自哪个域名。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。特别注意一点,此时服务器已经成功执行了这个请求,这里的报错只是在浏览器收到这个http回复后浏览器的报错行为。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

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

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com 可以请求数据。该字段也可以设为*,表示同意任意跨源请求。

如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。下面是"预检"请求之后,浏览器的正常CORS请求。头信息的Origin字段是浏览器自动添加的。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器正常的回应:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

简单请求下,服务器对于跨域请求是直接执行的;而非简单请求下,因为存在2阶段请求,服务器是否执行请求,是根据预检阶段的结果,浏览器是否发起第二次正式请求所决定。