专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  印度把 DeepSeek ... ·  3 天前  
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了! ·  2 天前  
OSC开源社区  ·  升级到Svelte ... ·  5 天前  
程序员的那些事  ·  惊!小偷“零元购”后竟向 DeepSeek ... ·  4 天前  
程序员小灰  ·  DeepSeek做AI代写,彻底爆了! ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

前端跨域解决方案

SegmentFault思否  · 公众号  · 程序员  · 2017-12-06 08:00

正文

浏览器同源策略

什么是浏览器同源策略?

“同源策略”(Same Origin Policy)是浏览器安全的基础。

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。

在判断两个页面的url是否具有相同的源之前,我们先来看一下一个url(统一资源定位符)的基本组成部分。

对于一个url,它的基本组成部分是:协议 :// 域名(或者是ip) : 端口(如果指定了) / 路径。

那么下面我们来举个例子:

对于 http://www.example.com/static 来说,协议就是 http ,它的域名是 www.example.com ,它的路径是 static ,这里的端口号省略了(默认为80)。

值得一提的是,在这个域名下面, example.com 是主域名,而 www.example.com 是子域名,同样的 a.example.com 也是一个与前面所述不同的另一个子域名。并且,上面这三个域名 互不相同

这里不再展开赘述,相关知识请查阅与计算机网络有关的知识。

那如何判断页面是否具有相同的源呢?

如果协议,端口(如果指定了)和域名对于两个页面是相同的,则两个页面具有相同的源。

也就是说,判断同源的三要素是: 协议 端口 域名

需要注意的是,如果是一个域名二级域名,比如上面提到的 www.example.com ,它与另外一个二级域名 a.example.com ,虽然他们的主域相同,但是子域不同,于是这两个就 不是同一个域名 ,所以也不能说是同源。三级域名依次类推...

下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:

关于Cookie和Session

说到同源策略,必不可少的就是Cookie这个东西了。

而讲到Cookie,跟它关联在一起的又有Session。

对于这两者,这里不做大篇幅的介绍,具体去传送门查阅。

这里我们做一下简要的总结:

  • Cookie受到浏览器同源策略的限制,A页面的Cookie无法被B页面的Cookie访问和操作。

  • Cookie最大存储容量一般为4KB,由服务器生成,在HTTP报文首部设置 Set-Cookie 可以指定生成Cookie的内容和生命周期,如果由浏览器生成则在关掉浏览器后失效。

  • Cookie存储在浏览器端,由于每次发送HTTP请求默认会把Cookie附到HTTP首部上去,所以Cookie主要用来身份认证,而不用来存储其他信息,防止HTTP报文过大。

  • Session存储在服务器,主要与Cookie配合使用完成身份认证和状态保持的功能。只有Cookie或只有Session都无法完成身份认证和状态保持的功能。

最后,对Cookie和Session实现的身份认证和状态保持功能做一个举例。

假设现在有一个学生信息管理系统,此时数据库已经有学生的相关信息(账号、密码、个人信息等等)。

然后当学生登录这个系统,通过POST请求把用户的账户密码发送到后台服务器。当后台服务器接收到这些参数的时候,会跟数据库保存的记录进行匹配。

一旦匹配成功,也就是用户的账号密码都正确的情况下,这个时候后台服务器会在Session中记录一个值,可以是用户名或者其他能够唯一标识用户的字段。

当把这个值保存在Session中后,后台服务器会返回响应告知客户端登录成功,可以进行后续的操作。此时,后台服务器会在HTTP响应报文中添加一个字段 Set-Cookie ,它的值是当前Session的SessionID,(这个SessionID是指向我们当前的那个Session的,在Node的Express中express-session会封装好这个过程)当然还会设置Cookie的其他属性,比如说过期时间 Expires 等等。

当浏览器接收到这个HTTP响应报文的时候,就会在本地设置一个Cookie,它的过期时间由响应报文中 Set-Cookie 中的 Expires 字段的值决定,如果为空,则关闭浏览器(即会话结束时)后失效。

之后,每次向后台服务器发送请求的时候,浏览器默认会把这个Cookie加在HTTP请求报文的Cookie中。这样,每次后台服务器接收到请求的时候,会根据Cookie中的SessionID去找到我们的Session。

假如这个SessionID映射得到Session,那么这个时候说明浏览器是已经登录过了。于是,就可以进行后续的一些相关的操作。

另外,值得一提的是, Session机制决定了当前客户只会获取到自己的Session,而不会获取到别人的Session。各客户的Session也彼此独立,互不可见。 也就是说,当多个客户端执行程序时,服务器会保存多个客户端的Session。获取Session的时候也不需要声明获取谁的Session。

这就是Cookie和Session做状态保持和身份验证的一个具体的例子。

关于iframe

说到同源限制,还有一个不得不提的就是iframe。

iframe可以在父页面中嵌入一个子页面,在日常开发中一旦使用,避免不了的就要涉及到不同的iframe页面进行通信的问题,可能是获得其他iframe的DOM,或者是获取其他iframe上的全局变量或方法等等。

同源下的iframe,也就是iframe中的 src 属性的URL符合同源的条件,那么通过iframe的 contentDocument contentWindow 获取其他iframe的DOM或者全局变量、方法都是很简单的事情。

那如果是非同源的两个iframe,单纯的通过变量访问的方式就受到同源限制了。

为了解决这个问题,HTML5引入了一个新的API: postMessage ,主要就是用来解决存在跨域问题的iframe页面之间通信的问题。

下面简单的举一个例子,假如现在有两个不同的页面,A页面的url是 http://localhost:4002/parent.html ,B页面的url的是 http://localhost:4003/child.html ,现在我把B页面用iframe嵌在A页面下面,代码(精简)是这样子的。现在我要实现的是向子页面B传递一个消息:

A页面代码:

  1.    

    A页面

  2.    

  3.    

  4.    

  5.     不受同源策略的限制。

开发时常用解决方案

CORS——跨域资源共享

注:以下跨域资源共享的两种请求辨析内容摘抄至阮一峰的《跨域资源共享》一文。

什么是跨域资源共享?

CORS是一个W3C标准,全称是“跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而客克服了AJAX只能同源发送请求的限制。

实现CORS主要在于服务器的设置,关键在于服务器HTTP响应报文首部的设置。前端部分大致还是跟原来发AJAX请求没什么区别,只是需要对AJAX进行一些相关的设置,稍后我们都会讲到。

CORS的两种请求

在讲解如何实现跨域资源共享的时候,我们先来看一下CORS的两种请求。

浏览器将CORS分为两种请求,一种是简单请求,另外一种对应的肯定就是非简单请求。

只要同时满足下面两大条件,就属于简单请求:

1.请求的方法是一下的三种方法之一。

  • HEAD

  • GET

  • POST

2.HTTP的头信息不超过以下几种字段:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type: 只限于三个值: application/x-www-form-urlencoded、 multipart/formdata、 text/plain

凡是不同时满足以上两种条件,就属于非简单请求。

浏览器对于两种请求处理是不一样的。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在HTTP请求报文首部,增加一个 Origin字段。如下:

  1. GET /cors HTTP/1.1

  2. Origin: http://api.bob.com

  3. Host: api.alice.com

  4. Accept-Language: en-US

  5. Connection: keep-alive

  6. User-Agent: Mozilla/5.0...

上面 Origin字段的用来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值,决定是否同意这次请求。

如果 Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的 onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

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

  1. Access -Control-Allow-Origin: http://api.bob.com

  2. Access-Control-Allow-Credentials: true

  3. Access-Control-Expose-Headers: FooBar

  4. Content-Type: text/html; charset=utf-8

上面的HTTP响应报文首部信息中,有三个与CORS请求相关的字段,都是以 Access-Control-开头。

Access-Control-Allow-Origin

该字段是必须的,它的值要么是请求 Origin字段,要么是一个 *,表示接受任意域名的请求。

Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为 true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送Cookie,删除该字段即可。

值得一提的是,如果想要CORS支持Cookie,不仅要在服务器指定HTTP响应报文首部字段,还需要在AJAX中打开 withCredentials的属性。(jQuery中AJAX设置后面会讲到)

  1. var xhr = new XMLHttpRequest();

  2. xhr.withCredentials = true;

有些浏览器在省略 withCredentials设置的时候,还是会发送Cookie。于是,可以显式关闭这个属性。

  1. xhr.withCredentials = false;

需要注意的是,如果要发送Cookie, Acess-Control-Allow-Origin不能设置为 *,必须设置成具体的域名,如果是本地调试的话可以考虑设置成 null

Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的 getResponseHeader() 方法只能拿到6个基本字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers里面指定。上面的例子指定, getResponseHeader('FooBar')可以返回 FooBar字段的值。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者 Content-Type字段的类型是 application/json

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

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

下面是一段JavaScript脚本:

  1. var url = 'http://api.alice.com/cors';

  2. var xhr = new XMLHttpRequest();

  3. xhr.open('PUT', url, true);

  4. xhr.setRequestHeader('X-Custom-Header', 'value');

  5. xhr.send();

很明显,这是一个非简单请求,使用了PUT方法来发送请求,并且自定义了一个HTTP请求报文的首部字段。

于是,浏览器发现这是一个非简单的请求,就自动发出了一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的HTTP头信息。

  1. OPTIONS /cors HTTP/1.1

  2. Origin: http://api.bob.com

  3. Access-Control-Request-Method: PUT

  4. Access-Control- Request-Headers: X-Custom-Header

  5. Host: api.alice.com

  6. Accept-Language: en-US

  7. Connection: keep-alive

  8. User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。

除了 Origin字段,“预检”请求的头信息还包括两个特殊字段。

Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS会用到哪些HTTP方法,上面是PUT。

Access-Control-Request-Headers

该字段是一个用逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息。上面的例子是 X-Custom-Header

于是,服务器收到“预检”请求之后,检查了 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨域请求,就可以做出回应。

  1. HTTP/1.1 200 OK

  2. Date: Mon, 01 Dec 2008 01:15:39 GMT

  3. Server: Apache/2.0.61 (Unix)

  4. Access-Control-Allow- Origin: http://api.bob.com

  5. Access-Control-Allow-Methods: GET, POST, PUT

  6. Access-Control-Allow-Headers: X-Custom-Header

  7. Content-Type: text/html; charset=utf-8

  8. Content-Encoding: gzip

  9. Content-Length: 0

  10. Keep-Alive: timeout=2, max=100

  11. Connection: Keep-Alive

  12. Content-Type: text/plain

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

  1. XMLHttpRequest cannot load http://api.alice.com.

  2. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下:

  1. Access-Control-Allow-Methods: GET, POST, PUT

  2. Access-Control -Allow-Headers: X-Custom-Header

  3. Access-Control-Allow-Credentials: true

  4. Access-Control-Max-Age: 1728000

对比简单请求服务器响应的CORS字段,发现多了三个:

Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

Access-Control-Allow-Headers

如果浏览器请求包括 Access-Control-Request-Headers字段,则 Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

于是,一旦浏览器通过了“预检”,以后每次浏览器正常的CORS请求,都跟简单请求一样,会有一个 Origin头信息字段。服务器的回应,也都有一个 Access-Control-Allow-Origin头信息字段。如果开启了Cookie设置,那还有一个 Access-Control-Allow-Credentials:true

如何在Node实现跨域资源共享?

那怎么在Node中结合Express设置后台的跨域部分呢?

其实很简单,需要设置的就是上面所述的几个响应首部的字段,主要考虑两种类型的请求和是否需要使用Cookie。具体设置如下:

  1. app.all("*", function(req, res, next) {

  2.    res.header("Access-Control-Allow-Origin", /* url | * | null */);

  3.    res.header("Access-Control-Allow-Headers", "Authorization, X-Requested-With");

  4.    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); /* 服务器支持的所有字段 */

  5.    res.header("Access-Control-Allow-Credentials", "true"); /* 当使用Cookie时 */

  6.    res.header("Access-Control-Max-Age" , 300000); /* 设置预检请求的有效期 */

  7.    if (req.method === "OPTIONS") return res.send(200); /*让options请求快速返回*/

  8.    else next();

  9. });

上面的设置有几个需要注意的地方:

  1. 如果需要本地调试,也就是在本地HTML页面发请求(类似 file://...之类的url),可以把 Access-Control-Allwo-Origin的值设置为Null,这样子就能够使用Cookie。如果设置成 *,虽然也可以跨域发送请求,但是这个时候没有办法使用Cookie

  2. Access-Control-Allow-Headers字段不是必须的,仅当发送的请求有 Access-Control-Request-Headers时需要设置。

  3. 使用Cookie的时候要配置 Access-Control-Allow-Methods字段。

  4. “预检”请求可设置缓存时间,可以保证不多次发送“预检”请求。

  5. 需要判断是否为“预检”请求,当拦截“预检”请求的时候直接返回。

如果使用jQuery封装的AJAX发送请求,那么需要在相应的JS代码设置:

  1. $.ajaxSetup({ xhrFields: { withCredentials: true }, crossDomain: true });

withCredentials是设置CORS发送Cookie,默认是不发送的。

crossDomain告知AJAX允许跨域。

以上就是CORS设置跨域的具体介绍。

基于JSONP技术实现跨域

JSONP原理

对于JSONP来说,前面也已经提到了,其实它是利用了某些不受同源限制的标签的所谓“漏洞”,来实现“曲线救国”式的跨域的方案。

它借用 script标签不受同源限制的这个特性,通过动态的给页面添加一个 script标签,利用事先声明好的数据处理函数来获取数据。

值得一提的是,JSONP这种方法其实和CORS有很大的区别,它并不属于一种规范。所谓的JSONP是应用JSON数据的一种新方法,它只不过是被包含在函数调用中的JSON。

在JSONP中包含两部分:回调函数和数据。其中,回调函数是当响应到来时要放在当前页面被调用的函数。而数据,就是传入回调函数中的JSON字符串,也就是回调函数的参数了。下面我们简单模拟一下JSONP的通信过程。

JSONP的原理详细讲解可以看这个传送门。

我们来简单的模拟一下JSONP的通信过程。

  1. function handleResponse(response) {

  2.    console.log(response.data);

  3. }

  4. var script = document.createElement("script");

  5. script.src = "http://example.com/jsonp/getSomething?uid=123&callback=hadleResponse"

  6. document.body.insertBefore(script, document.body.firstChild);

  7. /*handleResponse({"data": "hey"})*/

它的过程是这样子的:

  1. 当我们通过新建一个 script标签请求时,后台会根据相应的参数来生成相应的JSON数据。比如说上面这个链接,传递了 handleResponse给后台,然后后台根据这个参数再结合数据生成了 handleResponse({"data":"hey"})

  2. 紧接着,这个返回的JSON数据其实就可以被当成一个js脚本,就是对一个函数的调用。

  3. 由于我们事先已经声明了这么一个回调函数,于是当资源加载进来的时候,直接就对函数进行调用,于是数据当然就能获取到了。

  4. 至此,跨域通信完成。

另外,想要实现JSONP,后台服务器也必须做相应的设置。

值得一提的是,JSONP是存在一定的局限性的:

  • 只能用于GET请求

  • 存在安全问题,请求代码中可能存在安全隐患

  • 要确定JSONP请求是否失败并不容易

一个简单的JSONP实现

下面是一个实现JSONP的库,我们来一起分析一下它的源代码。这是源码的github地址。

  1. /**

  2. * Module dependencies

  3. */

  4. var debug = require('debug')('jsonp');

  5. //使用依赖

  6. /**

  7. * Module exports.

  8. */

  9. module.exports = jsonp;

  10. //输出模块

  11. /**

  12. * Callback index.

  13. */

  14. var count = 0;

  15. //回调函数的index值,便于取名。

  16. /**

  17. * Noop function.

  18. */

  19. function noop(){}

  20. //无操作空函数,以便使用后把window[id]置空

  21. /**

  22. * JSONP handler

  23. *

  24. * Options:

  25. *  - param {String} qs parameter (`callback`)

  26. *  - prefix {String} qs parameter (`__jp`)

  27. *  - name {String} qs parameter (`prefix` + incr)

  28. *  - timeout {Number} how long after a timeout error is emitted (`60000`)

  29. *

  30. * @param {String} url

  31. * @param {Object|Function} optional options / callback //这里的callback是取得数据后的callback,不是传给服务器的callback

  32. * @param {Function} optional callback

  33. */

  34. function jsonp(url, opts, fn){

  35.  if ('function' == typeof opts) {

  36.    fn = opts;

  37.    opts = {};

  38.  }

  39.  if (!opts) opts = {};

  40.  var prefix = opts.prefix || '__jp';

  41.  // use the callback name that was passed if one was provided.

  42.  // otherwise generate a unique name by incrementing our counter.

  43.  var id = opts.name || (prefix + (count++));

  44.  var param = opts.param || 'callback';

  45.  var timeout = null != opts.timeout ? opts.timeout : 60000;

  46.  var enc = encodeURIComponent;

  47.  var target = document.getElementsByTagName('script')[0] || document.head;

  48.  var script;

  49.  var timer;

  50.  //一定时间内后台服务器没有返回视为超时

  51.  if (timeout) {

  52.    timer = setTimeout( function(){

  53.      cleanup();

  54.      if (fn) fn(new Error('Timeout'));

  55.    }, timeout);

  56.  }

  57.  //回复原始设置、清空状态

  58.  function cleanup(){

  59.    if (script.parentNode) script.parentNode.removeChild(script);

  60.    window[id] = noop;

  61.    if (timer) clearTimeout(timer);

  62.  }

  63.  //取消操作

  64.  function cancel(){

  65.    if (window[id]) {

  66.      cleanup();

  67.    }

  68.  }

  69.  //声明函数,等待script标签加载的url引入完毕后调用

  70.  window[id] = function(data){

  71.    debug('jsonp got', data);

  72.    cleanup();

  73.    if (fn) fn(null, data);//node中约定第一个参数为err,但是这里不传,直接就置为null

  74.  };

  75.  // add qs component

  76.  url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id);

  77.  url = url.replace('?&', '?');

  78.  debug('jsonp req "%s"', url);

  79.  // create script

  80.  script = document.createElement('script');

  81.  script.src = url;

  82.  target.parentNode.insertBefore(script, target);

  83.  //引入script标签后会直接去调用声明的函数,然后函数会把script标签带有的data给传出去







请到「今天看啥」查看全文


推荐文章
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了!
2 天前
程序员小灰  ·  DeepSeek做AI代写,彻底爆了!
5 天前
周国平  ·  是什么扼杀了我们的天赋
8 年前
花儿小草  ·  有没有想过自己会生一场病
7 年前
经典短篇阅读小组  ·  料得重圆密誓,难禁寸裂柔肠
7 年前
黄生看金融  ·  战争,一触即发
7 年前