跨源通信、跨域访问

跨源通信、跨域访问

什么是跨源、跨域

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)端口号(port)。

同源策略是浏览器的一个安全功能,不同源的客户端脚本在未明确授权的情况下,不能读写对方的资源。

同源策略 是浏览器安全的基石。

同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互,例如办公内外网环境,当我们访问外网一个恶意网站的时候,恶意网站就会利用我们的主机向内网的 url 发送 ajax 请求,破坏或盗取数据。

浏览器的非同源限制、解决思路

非同源限制

  1. 无法读取非同源网页的 CookieLocalStorageIndexedDB
  2. 无法操作非同源网页的 DOM
  3. 无法向非同源地址发送 AJAX 请求,即 XHR 请求。

跨域的解决思路-3种

避免非同源限制—“躲着走”

  1. 让浏览器不做限制,指定参数,让浏览器不做校验,但该方法不太合理,因为它需要每个用户都去对浏览器设置做改动;
  2. 不要发出 XHR 请求,这样就算是跨域,浏览器也不会有非同源限制,解决方案是 JSONP ,通过动态创建一个 script ,通过 script 发出请求;

跨域资源共享方案

根据 W3C 的跨源资源共享方案,在被调用方修改代码,加上字段,告诉浏览器该网站支持跨域。

通过服务器反向代理,隐藏跨域

使用 Nginx 反向代理,在 a 域名中的请求地址使用反向代理指向 b 域名,让浏览器一直以为在访问 a 网站,不触发跨域限制。

JSONP

简单示例(JavaScript + PHP)

JavaScript

<script>
  var url="http://b.cn/do.php?callback=foo";
  //注意 foo函数的定义要先于 引入 url里的内容
  function foo(res){
      console.log(res);
      console.log(res.a);
  }

  function loadData(url){
      var elem=document.createElement('script');
      elem.src=url;
      document.getElementsByTagName('head')[0].appendChild(elem);
  }
  loadData(url);
</script>

PHP

$arr=['a'=>'ajax','b'=>'bbc'];
$callback=$_GET['callback'];
$json=$callback."(".json_encode($arr).");";
echo $json;   //foo({"a":"ajax","b":"bbc"});

上述代码中,相当于使用 PHP 结合前端的 JavaScript 拼接成一个 foo 方法的调用,也就是 loadData 方法最后返回的是 foo 函数的一个调用,也就是 foo({"a":"ajax","b":"bbc"});

Jquery中 JSONP 示例

这里使用到 Jquery 中两个参数:

  • Jsonp:回调函数的参数名,默认为 callback ,服务端凭借这个参数获得回调函数名称;

  • JsonpCallbackjsonp 请求成功后,在js 中调用的回调函数的名字,默认是 Jquery 自动生成。指定 jsonpCallback 时可以将回调函数写在 Ajax 外面做其他操作,不指定时不能这样做,只能在 SUCCESS 里做操作

    未指定 Jsonp、JsonpCallback 的名称

JavaScript
$.ajax({
    url: 'http://lifeloopdev.info/get_events',
    dataType: "jsonp",
    data: "offset=0&num_items=10",
    success: function (data) {
        $.each(data.success, function (i, item) {
            $("body").append('<h1>' + item.title + '</h1>');
        });
    }
});
PHP
header('content-type:text-html;charset=utf-8');
$callback = trim($_GET['callback']);
echo $callback + "{ \"success\": [{ \"id\": 1, \"title\": \"title 1\" }, { \"id\": 2, \"title\": \"title 2\" }, { \"id\": 3, \"title\": \"title 3\"}] }";

指定 Jsonp、JsonpCallback 的名称

JavaScript
$.ajax({
    url: 'http://lifeloopdev.info/get_events',
    dataType: "jsonp",
    data: "offset=0&num_items=10",
    jsonp: "selfCallback",
    jsonpCallback: 'successCallback'
});

function successCallback(data) {
    $.each(data.success, function (i, item) {
        $("body").append('<h1>' + item.title + '</h1>');
    });
}
PHP
header('content-type:text-html;charset=utf-8');
$callback = trim($_GET['selfCallback']);
echo $callback + "{ \"success\": [{ \"id\": 1, \"title\": \"title 1\" }, { \"id\": 2, \"title\": \"title 2\" }, { \"id\": 3, \"title\": \"title 3\"}] }";

JSONP 的弊端

  1. 需要服务器改动代码;
  2. 只支持 GET 请求;
  3. 发送的不是 XHR请求;
  4. 相对不安全。

后端解决跨域

跟用户数据有关的就是动态请求,没有数据的是静态请求,比如 css js,So,HTTP 服务器(Apache、Nginx 等)至少做了两个作用

  • HTTP 服务器,处理静态请求;
  • 反向代理,负载均衡。

在服务器端解决跨域有2种解决思路:

  1. 在被调用后端应用解决:在响应头增加指定字段,告诉浏览器,允许调用,这种解决方案的请求是直接从浏览器发送给后端服务器,在浏览器上会看到 b.comurl
  2. 在前端服务器解决:这是隐藏跨域的解决方案。这种跨域请求不是直接从浏览器发送的,而是从中间的 http 服务器(前端应用所在服务器)转发过去的,在浏览器中看到的还是 a.comurl,所以不会认为是跨域,但是该到 b.com 的请求还是会到 b.com

CORS解决跨域相关原理

为了解决浏览器跨域问题,W3C 提出了跨源资源共享方案,即 CORS(Cross-Origin Resource Sharing)

CORS 可以在不破坏即有规则的情况下,通过后端服务器实现 CORS 接口,就可以实现跨域通信。

CORS 将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。

简单请求

简单请求就是普通 HTML Form 在不依赖脚本的情况下可以发出的请求,比如表单的 method 如果指定为 POST ,可以用 enctype 属性指定用什么方式对表单内容进行编码,合法的值就是前述这三种。

  1. 在 CORS 出现前,发送 HTTP 请求时在头信息中不能包含任何自定义字段,且 HTTP 头信息不超过以下几个字段:
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Last-Event-ID
    5. Content-Type 仅为这3种
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
  2. 请求方法是 GET HEAD POST 且满足条件1
一个简单请求的🌰
GET /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, sdch, br
Origin: http://www.test.com
Host: www.test.com

对于简单请求,CORS 的策略是请求时在请求头中增加一个 Origin 字段,表示请求发出的域。

服务器收到请求后,根据该字段判断是否允许该请求访问。

  • 如果允许,则在 HTTP 头信息中添加 Access-Control-Allow-Origin 字段,并返回正确的结果
  • 如果不允许,则不添加 Access-Control-Allow-Origin 字段

除了上面提到的 Access-Control-Allow-Origin,还有几个字段用于描述 CORS 返回结果

  • Access-Control-Allow-Credentials:可选,用户是否可以发送、处理cookie
  • Access-Control-Expose-Headers:可选,可以让用户拿到的字段。有几个字段无论是否允许跨域都可以拿到的:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma

非简单请求

一般是发送 JSON 格式的 ajax 请求,或带有自定义头的请求。

非简单请求就是普通 HTML Form 无法实现的请求。比如 PUT 方法、需要其他的内容编码方式、自定义头之类的。

对于非简单请求的跨源请求,浏览器会在真实请求发出前,增加一次 OPTION 请求,称为预检请求(preflightrequest)

预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作

例如一个 GET 请求的预检请求,包含一个自定义参数X-Custom-Header

OPTIONS /test HTTP/1.1
Origin: http://www.test.com
Access-Control-Request-Method: GET // 请求使用的 HTTP 方法
Access-Control-Request-Headers: X-Custom-Header // 请求中包含的自定义头字段
Host: www.test.com

服务器收到请求时,需要分别对 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 进行验证,验证通过后,会在返回 HTTP 头信息中添加:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.test.com // 允许的域
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的方法
Access-Control-Allow-Headers: X-Custom-Header // 允许的自定义字段
Access-Control-Allow-Credentials: true // 是否允许用户发送、处理 cookie
Access-Control-Max-Age: 172800 // 预检请求的有效期,单位为秒。有效期内,不需要发送预检请求,ps 48小时

当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。

所以后端处理其实处理的就是这次预检后请求

<?php
    header('Content-Type: text/html;charset=utf-8');
    header('Access-Control-Allow-Origin:*'); // *代表允许任何网址请求
    header('Access-Control-Allow-Methods:POST,GET,OPTIONS,DELETE'); // 允许请求的类型
    header('Access-Control-Allow-Credentials: true'); // 设置是否允许发送 cookies
    header('Access-Control-Allow-Headers: Content-Type,Content-Length,Accept-Encoding,X-Requested-with, Origin'); // 设置允许自定义请求头的字段
if($_GET['name']) { 
  $name = $_GET['name'];
  echo $name;
} else { 
  echo "请求成功但。。。。";
} ?>

预检请求—CORS-preflight

对于服务器来说:

第一,许多服务器压根没打算给跨源用。当然你不给 CORS 响应头,浏览器也不会使用响应结果,但是请求本身可能已经造成了后果。所以最好是默认禁止跨源请求。

第二,要回答某个请求是否接受跨源,可能涉及额外的计算逻辑。这个逻辑可能很简单,比如一律放行。也可能比较复杂,结果可能取决于哪个资源哪种操作来自哪个 origin。对浏览器来说,就是某个资源是否允许跨源这么简单;对服务器来说,计算成本却可大可小。所以我们希望最好不用每次请求都让服务器劳神计算。

CORS-preflight 就是这样一种机制,浏览器先单独请求一次,询问服务器某个资源是否可以跨源,如果不允许的话就不发实际的请求。注意先许可再请求等于默认禁止了跨源请求。如果允许的话,浏览器会记住,然后发实际请求,且之后每次就都直接请求而不用再询问服务器否可以跨源了。于是,服务器想支持跨源,就只要针对 preflight 进行跨源许可计算。本身真正的响应代码则完全不管这个事情。并且因为 preflight 是许可式的,也就是说如果服务器不打算接受跨源,什么事情都不用做。

但是这机制只能限于非简单请求

在处理简单请求的时候,如果服务器不打算接受跨源请求,不能依赖 CORS-preflight 机制,因为不通过 CORS,普通表单也能发起简单请求,所以默认禁止跨源是做不到的。

既然如此,简单请求发 preflight 就没有意义了,就算发了服务器也省不了后续每次的计算,反而在一开始多了一次 preflight

简单请求为何不用预检请求?

有些人把简单请求不需要 preflight 理解为『向下兼容』。这也不能说错。但严格来说,并不是『为了向下兼容』而不能发。理论上浏览器可以区别对待表单请求和非表单请求 —— 对传统的跨源表单提交不发 preflight,从而保持兼容,只对非表单跨源请求发 preflight

但这样做并没有什么好处,反而把事情搞复杂了。比如本来你可以直接用脚本发跨源普通请求,尽管(在服务器默认没有跨源处理的情况下)你无法得到响应结果,但是你的需求可能只是发送无需返回,比如打个日志。但现在如果服务器不理解 preflight 你就干不了这个事情了。

而且如果真的这样做,服务器就变成了默认允许跨源表单,如果想控制跨源,还是得(跟原本一样)直接在响应处理中执行跨源计算逻辑;另一方面服务器又需要增加对 preflight 请求的响应支持,执行类似的跨源计算逻辑以控制来自非表单的相同跨源请求。服务器通常没有区分表单/非表单差异的需求,这样搞纯粹是折腾服务器端工程师。

所以简单请求不发 preflight 不是因为不能兼容,而是因为兼容的前提下发 preflight 对绝大多数服务器应用来说没有意义,反而把问题搞复杂了。

注意

在 Chrome 和 Firefox 中,如果 Access-Control-Allow-Methods 中并未允许 GET/POST/HEAD 请求,但允许跨域了,浏览器还是会允许 GET/POST/HEAD 这些 简单请求 访问,这时就必须在后台用其他办法禁掉这些 Method。

后端服务器–Nginx 解决方案

这里的 Nginx 仅做反向代理功能,浏览器访问页面在 a.comNginx 上,ajax 请求接口是 b.com,所以浏览器认为是跨域

Nginxnginx.conf 上配(vhost 是约定做法,这样做不修改主文件)

include vhost/*.config;

创建 cors.conf

server{
    listen 80; // 监听80端口
    server_name b.com; // 监听向 b.com 发送的请求
    location /{
        proxy_pass http://ser432ver.53253bb.com:8080; // 转发到哪里

        add_header Access-Control-Allow-Origin $http_origin; // $http_ 可以获取请求中相应的 header 参数
        add_header Access-Control-Allow-Method *;
        add_header Access-Control-Allow-Headers X-Custom-Header;
        // 或者
        // add_header Access-Control-Allow-Headers $http_access_control_request_headers;
        add_header Access-Control-Allow-Credentials true;
        add_header Access-Max-age 172800;

        // 直接处理预检命令,if 后要带空格
        if ($request_method = OPTIONS) {
            return 200;
        }
    }
}

前端服务器解决方案

但其实大部分情况下,我们会把前端应用和请求转发放在同一台 Nginx 上

server{
    listen 80; // 监听80端口
    server_name a.com; // 监听向 a.com 发送的请求

    location / {
        root   html;
        index  index.html index.htm;
    }

    locltion /ajaxserver {
        proxy_pass http://ser432ver.53253bb.com:8080; // 后端地址
    }
}

这样实质是隐藏跨域,让浏览器认为没有访问其他域就不会发生跨域。

前端代码需要在每个 ajax 请求前都要加上/ajaxserver

参考资料

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!