浏览器的跨域访问

图片来自nidan-455298会员

跨域问题是在web开发中一个常见的问题,其源自浏览器的同源安全策略,却影响着web网页对cookie/ajax/web storage等信息的跨域共享访问,本文将对同源策略、跨域问题的由来、影响的场景类别和常见解决方案进行了梳理,最后给出代码演示样例。

1. 同源安全策略

在浏览器中,有一个最核心和基本的安全策略,叫做同源策略(same-origin policy),它是指两个网页,若其协议、主机名、端口都相同的情况下,则两个网页属于同源,两者共享同源的数据存储,允许对共享数据的互通操作。

这个同源策略的概念最早由Netscape公司在1995年由其浏览器产品Netscape Navigator 2中引入,此后被所有浏览器采用。

为了理解同源策略,首先了解一个常见的网页url访问地址格式,如下,

scheme://host:port/path

其中:

  • scheme为通信协议,常见的有http/https/ftp等。
  • host为访问的主机名,常见的有域名、IP地址等。
  • port为通信端口号,若不指定的话,则为通信协议的缺省设置。
  • path为访问路径。

根据上面的概念,可以知道同源是指scheme + host + port三者都相同。下面我们看看哪些情况符合同源策略,哪些情况下不符合。

如下给出一些同源策略的规则判断样例,对标的网页:http://a.demo.com/dir/x.html

URL 是否同源 说明
http://a.demo.com/dir2/y.html 同源 只是路径不同,无影响
http://a.demo.com/dir/inner/y.html 同源 只是路径不同,无影响
http://a.demo.com:81/dir/z.html 非同源 端口不同
http://b.demo.com/dir/x.html 非同源 主机名不同,b.demo.com/td>
http://c.a.demo.com/dir/x.html 非同源 主机名不同,c.a.demo.com
https://a.demo.com/dir/x.html 非同源 通信协议不同,https
http://127.0.0.1/dir/x.html 非同源 主机名不同,即a.demo.com指向127.0.0.1的IP地址。
http://a.demo.com:80/dir/x.html 看情况 根据浏览器的实现
| URL | 是否同源 | 说明 |
| ---: | --- | --- |
| http://a.demo.com/dir/inner/y.html | 同源 | 只是路径不同,无影响|
| http://a.demo.com/dir2/y.html | 同源 | 只是路径不同,无影响 |
| http://a.demo.com:81/dir/z.html | 非同源 | 端口不同 |
| http://b.demo.com/dir/x.html | 非同源 | 主机名不同,b.demo.com |
| http://c.a.demo.com/dir/x.html | 非同源 | 主机名不同,c.a.demo.com |
| https://a.demo.com/dir/x.html | 非同源 | 通信协议不同,https |
| http://127.0.0.1/dir/x.html | 非同源 | 主机名不同,即a.demo.com指向127.0.0.1的IP地址。 |
| http://a.demo.com:80/dir/x.html | 看情况 | 根据浏览器的实现 |

2. 什么是跨域问题?

非同源的网页之间,由于同源安全策略的限制,一些数据信息(比如cookie\storage)会被浏览器隔离,JavaScript的Ajax请求也会被拦截并禁止。

但是,不同源的网页之间信息共享是很常见的场景需求,以典型的天猫电商网站为例,其有如下几个子域,

  • tmall.com 天猫电商根域名
  • list.tmall.com 天猫电商子域-列表
  • vip.tmall.com 天猫电商子域-会员
  • chaoshi.tmal.com 天猫电商子域-超市

上面各个子域页面互相属于非同源,受到同源策略的限制。

要实现这些子域之间数据信息访问共享,这就是本文要讨论的跨域问题。

3. 影响的场景类别

跨域问题本身是一个跨源的需求,根据同源策略的定义可知,这是在浏览器的应用场景中才会出现的问题。

浏览器在实现同源策略,保证网页安全的同时,也为跨源的信息共享提供各种途径和技术解决方案。为了方便讨论,根据浏览器的信息存储类别和获取方式,我们可以将跨域访问的需求划分如下三种,

  • cookie:获取另外一个域的cookie信息
  • ajax:通过ajax访问另外一个域的API接口,获取跨域接口数据
  • web storage:获取另外一个域的web storage存储信息

下面将分别对这三种应用场景下的跨域访问进行讨论。

4. Cookie的跨域访问

Cookie是在服务器端生成,发送给浏览器保存的一个键值对,其主要包括如下几个属性,

  • Key/Value:这是cookie的键名和值
  • Domain:这是cookie所属的域,比如.demo.com,则表明该cookie属于所有*.demo.com的域
  • Path:这是cookie所属的路径
  • Expires / Max-Age:过期的日期时间 / 最大存活时长
  • Secure:是否以安全方式传输cookie,以防止中间人攻击
  • HttpOnly:是否只能以Http/Https方式传输cookie,使得JS脚本无法获取,防止XSS攻击

上面Domain/Path的两个属性控制着cookie可以共享的域和路径。需要注意的是,cookie只和域+路径有关,和端口无关,例如a.demo.com:80和a.demo.com:81其实属于同一个域,两者共享cookie。

下面我们看下cookie的共享策略和规则判断。

假如对标的一个网页为:http://a.demo.com/dir/x.html,其页面的cookie属性设置为,

  • domain = .demo.com
  • path = /

那么这个网页上的cookie和如下网页的共享关系见下表,

URL 是否共享 说明
http://a.demo.com/dir2/y.html 共享 路径不同,无影响
http://a.demo.com/dir/inner/y.html 共享 路径不同,无影响
http://a.demo.com:81/dir/z.html 共享 端口不同,无影响
http://b.demo.com/dir/x.html 共享 二级域名不同,无影响
http://c.a.demo.com/dir/x.html 共享 三级域名不同,无影响
https://a.demo.com/dir/x.html 共享 通信协议为HTTPS,无影响
http://127.0.0.1/dir/x.html 不共享 主机名不同,无法共享。
http://a.demo.com:80/dir/x.html 共享
| URL | 是否共享 | 说明 |
| ---: | --- | --- |
| http://a.demo.com/dir/inner/y.html | 共享 | 路径不同,无影响 |
| http://a.demo.com/dir2/y.html | 共享 | 路径不同,无影响 |
| http://a.demo.com:81/dir/z.html | 共享 | 端口不同,无影响 |
| http://b.demo.com/dir/x.html | 共享 | 二级域名不同,无影响 |
| http://c.a.demo.com/dir/x.html | 共享 | 三级域名不同,无影响 |
| https://a.demo.com/dir/x.html | 共享 | 通信协议为HTTPS,无影响 |
| http://127.0.0.1/dir/x.html | 不共享 | 主机名不同,无法共享。 |
| http://a.demo.com:80/dir/x.html | 共享 | - |

可以对上表和上一个同源策略规则判断表,两者有很大的差别。

除了设置每个cookie的domain属性,两个网页之间还可以设置全局文档属性document.domain,实现cookie共享。若document.domain = demo.com,则下面两个网页的cookie共享,

  • http://a.demo.com/dir/x.html
  • http://b.demo.com/dir/y.html

5. Ajax的跨域请求

Ajax是一个异步请求访问后端API接口的方法,受到同源策略的影响,在请求跨域的接口时,会收到如下的错误消息(Chrome浏览器),

Failed to load http://a.demo.com/test No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://a.demo.com' is therefore not allowed access.

可以看到,浏览器禁止了Ajax的跨域请求。

为了实现Ajax的跨域请求,一般有如下两种途径,

  • JSONP:JSON with Padding,数据信息是JavaScript脚本获取
  • CORS:Cross-Origin Resource Sharing跨源资源共享,通过配置服务器实现跨域支持

此外,HTML5还引入了异步通信WebSocket技术,其专门用于长连接实时数据的通信,它一直作为Ajax的异步替代方案应用于异步请求。WebSocket其由于在协议中自带origin字段,所以不受同源策略限制,其跨域的安全主要由后端服务器控制。但是目前WebSocket的浏览器兼容性没有Ajax技术支持的好,应用推广还比较受限。

5.1 JSONP

英文全称为JSON with Padding,数据信息是通过一段JavaScript脚本进行传输,其信息由JS编译器识别,而不是通过JS解析器解析。

JSONP的使用方法很简单,其首先在HTML DOM中通过JavaScript代码动态添加一个script元素,

<script type="text/javascript" src="http://a.demo.com/test"></script>

这个script元素指向一个跨源的API接口地址。浏览器发现有一个新增script元素后,会向这个地址发送一个GET请求,而服务器则会返回一个JavaScript脚本的消息给浏览器,例如,

alert("Hello, This is message from a.demo.com.");

上面的"Hello, This is message from a.demo.com."就是跨域获取的数据信息。浏览器在拿到返回消息后,执行该JS脚本。上面的脚本将会在浏览器中弹出一个提示框。需要注意的是,执行的函数可以通过参数传给服务器,

<script type="text/javascript" src="http://a.demo.com/test?callback=test"></script>

则服务器可以将返回的JavaScript脚本变为,

test("Hello, This is message from a.demo.com.");

JSONP的方式简单易用,兼容所有浏览器,主要缺点是只能应用于GET请求。

5.2 CORS

英文全称为Cross-Origin Resource Sharing,中文意思为跨源资源共享,这是一个W3C的标准,其支持所有类型的HTTP请求跨域调用,包括GET/PUT/POST/DELETE等等。

CORS需要浏览器和服务器的支持,目前绝大多数的浏览器都支持该功能,所以只需要配置好服务器的支持即可。

整个CORS的大概流程为,即将发起一个跨Ajax调用时,当浏览器发现这是一个跨域请求,会在请求头中添加Origin字段,表明该请求的来源,并先向服务发起一次查询,询问是否该Ajax请求调用是否合法,若合法,则像和正常Ajax请求一样执行(请求头中会添加Origin字段)。

请求头中的Origin需要和服务器上接口配置的Access-Control-Allow-Origin/Access-Control-Allow-Methods相匹配,下面是给出几个样例的匹配判断。

CORS的接口配置 说明 请求样例
Access-Control-Allow-Origin: http://a.demo.com/test
Access-Control-Allow-Methods: GET, PUT
允许来自指定origin的GET/PUT请求 GET http://a.demo.com/test
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
允许来自任何域的任何请求 路径不同,无影响

有时候希望服务器允许多个origin的请求,按照W3C的建议,这个一般在服务器动态配置,如下是一个实现的简单代码样例,

$http_origin = $_SERVER['HTTP_ORIGIN'];

if ($http_origin == "http://a.demo.com" || $http_origin == "http://b.demo.com" || $http_origin == "http://c.demo.com")
{  
    header("Access-Control-Allow-Origin: $http_origin");
}

6. Web Storage的跨域访问

浏览器的Web存储分为如下几种,

  • Local Storage:通过key/value形式进行的数据存储,没有时间限制
  • Session Storage:在一个会话中的数据存储,一旦重启浏览器,则数据会消失
  • IndexedDB:索引数据存储(HTML5引入)

由于同源策略的影响,非同源的网页之间的存储信息是相互隔离的。同时,同源安全策略禁止了不同域的网页之间不能进行相互的DOM直接操作,以防止xss安全攻击。这些都有效地保证网页的安全和隐私,但另一方面,网页之间的沟通交互也收到了限制,即使已知两个网页之间的通信是安全的。

为了保持同源安全策略,同时实现跨域的网页通信,在HTML5中引入了cross-document messaging(跨文档通信)的功能,使得网页之间消息通信成为可能。

一个常见的做法是,在当前页面中嵌入一个隐藏的iframe窗口,这个iframe引入另外一个域的网页。通过父窗口和iframe窗口的跨文档通信,实现存储信息的相互访问读取。整个通信主要使用到两个API,(下面是对API的简要说明)

window.postMessage(message, targetOrigin);

第一参数为发送的消息,第二参数为接受窗口的origin,例如"http://a.demo.com",或者设为"*",表示不限制接受窗口的origin,

window.addEventListener(type, receiver, userCapture);

第一个参数为消息类别,第二个参数为回调函数,第三个参数为在DOM树中是否优先于它下方的任何其它事件目标。

父窗口代码,

$("#btn_send_msg").click(function () {
   let iframe = document.getElementsByTagName('iframe')[0];
   iframe.contentWindow.postMessage('Hello world', '*');
});

iframe窗口代码,

window.addEventListener('msg', function (msg) {
  if (msg.origin == window.parent) {
    localStorage.setItem('message', msg.data);
    msg.source.postMessage('message is saved into local storage.', msg.origin);
  }        
}, false);

可以看到,我们通过cross-document messaging这个消息通道,可以实现跨域的存储信息共享。

7. 小结

下面对浏览器的跨域解决方案做个总结,

  • Cookie
    • 通过设置Cookie的共享域属性 domain = .demo.com。
    • 通过设置网页文档的共享域属性document.domain = demo.com。
  • Ajax
    • JSONP:通过加载JavaScript脚本实现跨域的GET请求。
    • CORS:通过配置服务器的CORS,接受跨域请求。
    • WebSocket:作为Ajax的替代方案,无跨域限制。
  • Web Storage
    • Cross-Document Messaging:通过隐藏的iframe窗口,打开跨域访问的通信通道,实现对跨域的存储信息访问。

8. 演示代码样例

演示代码样例见下面的git代码仓库地址,详细使用说明见工程README文件。

https://gitee.com/pphh/simple-demo/tree/master/demo-cors

9. 参考文献

  1. Wiki – Same Origin Policy
    https://en.wikipedia.org/wiki/Same-origin_policy

  2. MDN – Same Origin Policy
    https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

  3. Uniform Resource Locators (URL)
    https://tools.ietf.org/html/rfc1738

  4. Wiki - Cookie
    https://en.wikipedia.org/wiki/HTTP_cookie

  5. Wiki – Cross Site Scripting (XSS)
    https://en.wikipedia.org/wiki/Cross-site_scripting

  6. W3C – WebSockets
    https://www.w3.org/TR/websockets/

  7. W3C – CORS
    https://www.w3.org/TR/cors/

  8. W3C - 多个origin的CORS配置
    https://www.w3.org/TR/cors/#resource-implementation

  9. W3C - HTML5 Web Messaging
    https://www.w3.org/TR/webmessaging/

  10. W3C - HTML5
    https://www.w3.org/TR/html5/

  11. 阮一峰 - 浏览器同源政策及其规避方法
    http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

读舒乙的《作家老舍》

老舍

老舍(1899-1966年),原名舒庆春,字舍予,“舍予”两字合在一起为舒,分开则为“舍身,奉献自我”的意思。《作家老舍》一书由老舍之子舒乙执笔,里面以回忆、纪实、考证的方式记录了老舍的生平事迹,读这本书可以一览20世纪初中叶以老舍为代表的一代作家在当时动荡的社会文化变革中成长的足迹,了解老舍在中国现代文学和现代话剧上的创作历程。

1. 老舍的幼年

老舍诞生于1899年,那一年前后发生着几件大事,前面一年戊戌变法(1898年),后面一年八国联军以镇压义和团运动之名,进军北京(1900年),而后1901年清政府签订《辛丑条约》,从此清政府成为帝国主义傀儡,中国也进入了半封建半殖民地国家。

老舍的父亲在抵抗八国联军侵略北京城中阵亡,失去父亲的老舍从此和母亲相依为命,在北京的小羊圈胡同度过了他清贫的童年。

2. 辛亥革命

1911年爆发的辛亥革命推翻了清政府,带来了民主和共和,整个中华民族思想洪流涌动,中国社会变革加速。而当时出现的一个新鲜事,便是师范学校,其以培养小学老师为目的,课程设置效仿日本的师范中等学校。入读师范学校,一切都是免费,学校管吃管住管穿管学费管分配,全部都由国家政府出资负责。老舍在1913年考入北京师范学校,这是辛亥革命带给老舍的一件礼物,完全改变了他的人生命运,从此离开穷苦的家,走向文学的旅途。那一年他14岁。

3. 前往英国教学

1918年老舍从北京师范学校毕业,任命北京的第十七小学校长,老舍倾心投入教育事业。至1924年,在宝广林和英国人易文思先生的推荐下,老舍前往英国伦敦大学东方学院,被聘任为中文讲师,之后在英国度过5年的中文授教,其担任的课程主要有标准中国官话、中国古典文学、古文、道教和佛教文选等,年薪300英镑。在此期间,老舍为中国文化在西方的传播作出了重要的贡献,包括帮助艾支顿翻译中国古典名著《金瓶梅》,以及为汉语教学录制的《言语声片》。老舍成为中国文化在西方传播的一座桥梁。同时,老舍在英国的所见所闻为其后来写的长篇小说《二马》提供丰富的材料来源。

在1929年,老舍离开英国,在欧洲旅行三个多月,分别游历法国、荷兰、比利时、瑞士、德国和意大利等欧洲国家。于1929年秋坐船抵达新加坡,在华侨中学教书半年后,于1930年5月返回故乡北平。

4. 三部小说

老舍从1928年到1931年,分别写出三部长篇小说《老张的哲学》《赵子曰》《二马》,其以白话文的文体,并以北京话的风格,在当时的文坛刮来一股清风,读来赏心悦目,老舍的作品成文知识分子读者的新爱。三部作品都以强烈的爱国主义为主题,以幽默的形式反映社会的落后,却表达着对国家的忧国忧民、图强图变的热望。这三本小说的问世大体上构成了老舍独特的文学风格,从此基本上一直延续了他的一生。

“老舍”的笔名最开始出现在1926年在商务印书馆出版的《小说月报》上,发表小说《老张的哲学》。

在中国现代文学史有着“鲁茅郭巴老曹”的说法,老舍以这三部长篇小说为始,开创了属于自己的文学天地,为20世纪初的中国文坛作出了突出贡献。

5. 国难中成立的中国文协

1937年7月7日,日军发动“七七”事变,日本发动全面的侵华战争。随着战局的发展,平津和沪宁沦陷,华北和华东的作家撤退到武汉,加上华南来的作家,一时间,武汉聚集了来自北、东、南三个方向的七八百名文化人。国难当头,文艺家云集武汉三镇,大家不分政治派别,一致对外。

在1937年年底,老舍应周恩来、王明同志和国民党左派领袖冯玉祥将军的邀请,参加“文协”的筹备工作,在连开了6次筹备会议后,吸收了左中右各派文人,终于在1938年3月27日成功地召开了成立大会,发起人共97人,并通过了《中华全国文艺界抗敌协会宣言》(注1)。下面是宣言的一段节选(注2),

九一八与一二八后,文艺界无日不在忧心国防,因而时时将东北四省人民的苦痛,与侵略者的暴行,血淋淋的提献在全国的同胞的眼前,以期共赴国难,重整山河。成绩若何?未敢自是;救亡图存,咸具此心。

这宣言表达着当时文艺界在国难面前,发出匹夫有责的担当和吼声。

中华全国文艺界抗敌协会的成立是当时文艺界空前大团结的真实写照。在文协成立后,老舍当选为总务部主任,相当于协会会长,负责协会的日常工作和分会的发展,一直到1947年赴美后,在他的建议下,由叶圣陶接任会长职位(注3)。

6. 赴美放青

老舍在1946年初到1949年底,应美国国务院的邀请,拜访美国。

此行是在美国驻华大使馆文化联络员的威尔马·费正清和在重庆美国新闻处服务的约翰·金·费正清的引荐下,美国国务院邀请中国进步知识分子访美,一共邀请了两位,一个是曹禺,另外一位就是老舍。在之前,有郭沫若和矛盾先生的访苏之行。这是美苏两个大国为了争夺中国进步知识分子所作的举措,一方面为了文化交流,另一方面谋求中国知识分子对自己的支持。

在这期间,老舍在各个大学讲演,在国外讲述中国的现代小说和抗战文学,一边写作翻译,一边大量阅读美国文学作品,结识美国友人,过着忙碌紧张而又孤独的生活。在美国的三年半,对老舍的创作有着重大影响,美国的社会背景,以及话剧、电影和歌舞,让老舍对文化的多元思考提供源泉。

到1949年北平解放,老舍收到周恩来以及郭沫若、茅盾、巴金等老朋友联名来信,老舍归国。

7. 解放后的老舍和人民艺术剧院

老舍归国后,在北京的一个小四合院里生活度过了16年,在归国后的时间里,老舍持续写作,发表了十几部作品。其中,老舍和人民艺术剧院有着密切的合作,几乎每隔一年就创作一出话剧并上演,先后有《龙须沟》《青年突击队》《春华秋实》《红大院》《女店员》和《茶馆》等话剧登上人艺的剧台,把中国的话剧推上新高度。

8. 舍身

1966年文革中,老舍投太平湖舍身,在一个傍晚的湖边,留下了自己最后的足迹。

9. 参考文献和注解