跨域,处理好它其实很简单

0x01 前言

  跨域是前端开发中一个老生常谈的问题。 当今日益复杂的应用场景下,经常会遇到在不同子域名下的不同Web应用访问同一个专用API服务的情况。然而出于安全性考虑,非同源的Ajax请求会被浏览器拦截,导致这一请求失败,无法正常工作。

0x02 跨域策略

  什么是同源?为什么Ajax只能同源访问?

  所谓同源,是指“协议+域名+端口 ”三者相同,即便两个不同的域名指向同一个IP地址,也非同源。以下域名均互为不同源:

http://www.abc.com/xxx
http://abc.com/xxx
http://abc.com:8080/xxx
http://www.abc.com:8080/xxx
https://www.abc.com/xxx

  同源策略(SOP, Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。受到同源策略的约定,当请求非同源的资源时,浏览器将会自动拦截一下资源的请求:

  • AJAX 请求
  • DOM 和 Js对象操作(一般是<frame>、<iframe>或者是window.open创建的新窗口等)
  • Cookie、LocalStorage 和 IndexDB

0x03 跨域传递信息解决方案

1. JSONP —— 一个常见的跨域解决办法

  JSONP说白了就是利用<script>标签的src属性可以跨域(比如你引用了某个CDN上的jQuery之类的是可以的),请求一段脚本,脚本内容只有一个函数的调用。当它被加载的时候,这个函数调用实际上就是调用了目标页面上的函数,从而目标页面获取到服务器生成数据内容。

 <script>
    var script = document.createElement('script');
    script.src = 'http://www.abc.com/hello?callback=helloWorld';
    document.head.appendChild(script);
    // 当脚本被加载成功时,这个函数会被调用
    var helloWorld = function(res) {
        console.log(res); 
    }
 </script>

  而http://www.abc.com/hello这个接口返回的内容如下

helloWorld({"msg" : "Hello World!"});

  jQuery等框架内对JSONP的操作也基本如此,不做赘述。

2. iframe

  document.domain + iframe:利用两个页面同时设置相同的document.domain值来实现强行同源

<!--父页面 http://www.abc.com/-->
<iframe id="iframe" src="http://hello.abc.com/a.html"></iframe>
<script>
    document.domain = 'abc.com';
    var msg = 'Hello World!';
</script>
<!--子页面 http://hello.abc.com/a.html-->
<script>
    document.domain = 'abc.com';
    console.log(window.parent.msg);
</script>

  location.hash + iframe:如通过http://www.abc.com/xxx#hello=world请求子页面。子页面解析传入的location.hash的值即#hello=world,获取父页面传递的内容。这个方法局限性显而易见。

  window.name:也是一种奇特的办法,支持传递更多的内容。

3. postMessage

  HTML5新增的跨域传递信息的办法,参考:
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

0x04 跨域资源共享——CORS

  终于说到这个重头戏了:跨域资源共享(Cross-Origin Resource Sharing, CORS)

  它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了只能同源使用的限制。它需要服务器和浏览器同时支持,比如IE10以下的就免了吧。由于真个通信过程都是浏览器自动完成的,用户几乎感知不到它的存在。

  就以我的WeJudge项目为例:主站是www.wejudge.net,API接口的域名是api.wejudge.net,比赛服域名是contest.wejudge.net。对于绝大多数的API的请求,必定是需要带上Cookie信息来让服务器获知当前用户的Session和登录状态。

  此时,我需要为Axios的全局设定中加入withCredentials选项表示需要发送一个复杂的请求。由于是非同源请求,浏览器会预先向服务器发起OPTIONS方法的“预检”请求,以判断这个请求是否能够被执行。“预检”请求包括以下字段:

  • Access-Control-Request-Method 必填字段,指定有效的请求方法。如:GET
  • Access-Control-Request-Headers 可选字段,指定浏览器CORS请求会额外发送的头信息字段。

  这时候,服务器就需要作出一个正确的应答来告知浏览器这个跨域操作是合法的。我们先来了解几个常用的响应头字段:

  1. Access-Control-Allow-Origin 必填字段,它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  2. Access-Control-Allow-Credentials 可选字段,它的值是一个布尔值,表示是否允许发送Cookie。
  3. Access-Control-Allow-Methods 必填字段,表示支持的请求方法,多个以逗号分隔。
  4. Access-Control-Allow-Headers 可选字段,表示支持的请求头字段,多个以逗号分隔。
  5. Access-Control-Max-Age 用来指定本次预检请求的有效期,单位为秒。

  我们注意到一个问题, 当Access-Control-Allow-Credentials为true的同时,Access-Control-Allow-Origin被设置为*,出于安全性考虑,请求将被浏览器拒绝。而Access-Control-Allow-Origin不能用逗号等方式设定多个可用的源域名,如何进行跨域也就是接下来要讨论如何解决的一个问题。

  通过Nginx的配置文件,我们可以读取到请求头中的origin字段,并对其进行处理。如果源符合特定规则,则将其作为Access-Control-Allow-Origin的值发回给浏览器,从而使得跨域请求被正常处理。当然这个操作也可以在服务端程序上实现,但如果使用Nginx进行拦截处理,性能要更优于在其他服务端处理,毕竟我们希望服务端能够做更多适合它的事情,而不是做这些“杂活儿 ”。

  下面给出了一段Nginx配置,希望能对你有一些启发:

set $cors '';
set $cors_domain '';
set $access_origin '';
set $access_methods '';
set $access_age '';
set $access_headers '';
set $access_cred '';
if ($http_origin != '') {
        set $cors_domain 'forbid';
}
# 判断来源域名是否符合要求
if ($http_origin ~ '^https?://(.*?\.)?(domain.com||domain2.net)(\:\d{0,5})?$') {
        set $cors 'true';
        set $cors_domain 'yes';
}
# 如果来源域名不符合要求,则拒绝本次响应
if ($cors_domain = 'forbid') {
        add_header Content-Type 'text/plain';
        return 403 "Forbidden.";
}
if ($request_method = 'OPTIONS') {
        set $cors 'options';
}
# 如果是正常请求
if ($cors = 'true') {
        set $access_origin '$http_origin';
        set $access_cred 'true';
}
# 如果是预检请求
if ($cors = 'options') {
        set $access_origin '$http_origin';
        set $access_methods 'GET,POST,PUT,DELETE,OPTIONS';
        set $access_age '86400';
        set $access_cred 'true';
        set $access_headers 'Origin, Content-Type, X-Requested-With,Cache-Control,Content-Language,Expires,Last-Modified,Pragma';
        return 204;
}

add_header Access-Control-Allow-Origin "$access_origin" always;
add_header Access-Control-Allow-Credentials "$access_cred" always;
add_header Access-Control-Allow-Methods "$access_methods" always;
add_header Access-Control-Max-Age "$access_age" always;
add_header Access-Control-Allow-Headers "$access_headers" always;

  至此,跨域问题得到了较为全面的解决。当然,如果你愿意,可以把它配置得更加安全,顺便防范常见的XSS、CSRF攻击。

请求返回了正确的CORS头

如果你想要了解更加详细的内容,可以阅读阮一峰大神的博客。

参考文献:《跨域资源共享 CORS 详解》,阮一峰

http://www.ruanyifeng.com/blog/2016/04/cors.html