浏览器:深度理解浏览器的同源策略
概述
同源策略 是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载得脚本如何能与另一个源的资源进行交互。它能帮助阻挡恶意文档,减少可能被攻击的媒介 [ MDN ] .
同源策略 是为了保护用户信息,用户信息分为两种:
- 存在用户本地的信息,如cookie;
- 存在服务器数据库的用户信息,如个人资料;
所以 同源策略 限制 cookie 等信息的跨源网页读取,是为了保护本地用户信息; 同源策略 限制跨域 ajax 请求,是为了保护被跨域请求的服务器中数据库用户信息。
简单来说,限制读写跨域 cookie 是为了保护自己的信息;而限制跨域 ajax 请求,是为了保护别人的信息。跨源请求的本质是请求别人的信息,所以能否跨域请求,是由被请求的服务器决定的。
* 小知识:服务器间的访问不存在同源策略,因此往往通过代码做白名单或权限设置,如小程序的认证与上线一样,载体 API 会对认证后的程序开放 权限 / 白名单。通过 proxyTable 、Nginx 等代理服务器进行跨域,本质是服务器请求另一台服务器,因此不存在浏览器的同源策略。
同源的定义
如果两个 URL 的 protocol 、port (en-US) (有指定时)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为 “ 协议 / 主机 / 端口元组 ”,或者直接是 “ 元组 ” 。(“ 元组 ” 是指一组项目构成的整体,双重 / 三重 / 四重 / 五重 / 等的通用形式)
下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:
URL | 结果 | 原因 |
http://store.company.com/dir2/other.html |
同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html |
同源 | 只有路径不同 |
https://store.company.com/secure.html |
失败 | 协议不同 |
http://store.company.com:81/dir/etc.html |
失败 | 端口不同 ( http:// 默认端口是80) |
http://news.company.com/dir/other.html |
失败 | 主机不同 |
另一组例子(同源):
http://example.com/app1/index.html http://example.com/app2/index.html |
same origin because same scheme (http ) and host (example.com ) |
http://Example.com:80 http://example.com |
same origin because a server delivers HTTP content through port 80 by default |
非同源:
http://example.com/app1 https://example.com/app2 |
different schemes |
http://example.com http://www.example.com http://myapp.example.com |
different hosts |
http://example.com http://example.com:8080 |
different ports |
非同源请求报错:
这时后端会给你反馈:接口服务已经接收到请求并成功返回!!这时就要回归文题了:浏览器的 同源策略。
服务器是不存在同源策略的,也就是我们平时通过代理服务器访问非同源服务后再将响应数据返回给前端的理由了。
那为什么浏览器存在同源策略服务器仍然能接收到请求并作出响应?因为浏览器并没有拦截我们的跨域请求,而是偷摸截胡了服务器返回给我们的响应,浏览器它拿到了,但是不给我们,就是玩儿。
为什么要进行跨域
当一个项目变大时,把所有的内容架构在一个网站或者后台服务器是不现实的。
一个体量很大的网站往往有许多独立且复杂的业务,比如分为三个独立服务模块:
- 订单管理api
- 用户管理api
- 新闻管理api
此时我们把 web 项目和任何一个 API 服务集成在一起都不合适,它应该是一个专门的网站。
源的继承
在页面中通过 about:blank 或 javascript: URL 执行的脚本会继承打开该 URL 的文档的源,因为这些类型的 URLs 没有包含源服务器的相关信息。
例如,about:blank
通常作为父脚本写入内容的新的空白弹出窗口的 URL(例如,通过 Window.open() )。 如果此弹出窗口也包含 JavaScript,则该脚本将从创建它的脚本那里继承对应的源。
注意:在Gecko 6.0之前,如果用户在位置栏中输入 data
URLs,data
URLs 将继承当前浏览器窗口中网页的安全上下文。
data
:URLs 获得一个新的,空的安全上下文。
IE中的特例
Internet Explorer 的同源策略有两个主要的差异点:
- 授信范围 ( Trust Zones ) :两个相互之间高度互信的域名,如公司域名 ( corporate domains ) ,则不受同源策略限制。
- 端口:IE 未将端口号纳入到同源策略的检查中,因此 https://company.com:81/index.html 和 https://company.com/index/html 属于同源并且不受任何限制。
这些差异点是不规范的,其它浏览器也未做出支持。但会助于开发基于 window RT IE 的应用程序。
源的更改
满足某些限制条件的情况下,页面时可以修改它的源。脚本可以将 document.domain 的值设置为其当前域或其当前域的父域。如果将其设置为其当前域的父域,则这个较短的父域将用于后续源检查。
例如:假设 http://store.company.com/dir/other.html 文档中的一个脚本执行以下语句:
document.domain = "company.com";
这条语句执行之后,页面将会成功地通过与 http://company.com/dir/page.html 的同源检测(假设 http://company.com/dir/page.html 将其 document.domain 设置为 " company.com ",以表明它希望允许这样做 - 更多有关信息,请参阅 document.domain )。然而, company.com 不能设置 document.domain 为 othercompany.com ,因为它不是 company.com 的父域。
端口号是由浏览器另行检查的。任何对 document.domain 的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null 。因此 company.com:8080 不能仅通过设置 document.domain = "company.com" 来与 company.com 通信。必须在他们双方中都进行赋值,以确保端口号都 null 。
注意:使用 document.domain
来允许子域安全访问其父域时,您需要在父域和子域中设置 document.domain 为相同的值。这是必要的,即使这样做只是将父域设置回其原始值。不这样做可能会导致权限错误。
跨源(域)网络访问
同源策略控制不同源之间的交互,例如在使用 XMLHttpRequest 或 <img> 标签时会受到同源策略的约束。这些交互通常分为三类:
- 跨域写操作 ( Cross-origin writes ) 一般是被允许的。例如链接(links),重定向以及表单提交。特定少数的HTTP请求需要添加 preflight 。
- 跨域资源嵌入 ( Cross-origin embedding ) 一般是被允许(下文会举例说明)。
- 跨域读操作 ( Cross-origin embedding ) 一般是不被允许的,但常可以通过内嵌资源来巧妙的进行读取访问。例如,你可以读取嵌入图片的高度和宽度,调用内嵌脚本的方法,或 availability of an embedded resource .
以下是可能嵌入跨源的资源的一些示例:
- <script src="..."></script> 标签嵌入跨域脚本。语法错误信息只能被同源脚本中捕捉到。
- <link rel="stylesheet" href="..."> 标签嵌入 CSS 。由于 CSS 的 松散的语法规则 , CSS 的跨域需要一个设置正确的 HTTP 头部 Content-Type 。不同浏览器有不同的限制: IE 、Firefox 、Chrome 、Safari(跳至 CVE-2010-0051)部分 和 Opera 。
- 通过 <img> 展示的图片。支持的图片格式包括 PNG , JEPG , GIF , BMP , SVG , ...
- 通过 <video> 和 <audio> 播放的多媒体资源。
- 通过 <object> 、<embed> 和 <applet> 嵌入的插件。(注意,<embed> 是 HTML 5 中的新标签,现在已经不建议使用了,可用 <img> 、<iframe> 、<video> 、<audio> 等标签代替;HTML 5 不支持 <applet> 标签,在 HTML 4.01 中该元素已废弃,请使用 <object> 标签代替它)
- 通过 @font-face 引入的字体。一些浏览器允许跨域字体 ( cross-origin fonts ),一些需要同源字体(same-origin fonts)。
- 通过 <iframe> 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。
通俗来讲,以上这些方式是不需要通过代理即可对他人的资源进行跨站读取的。
如何允许跨源访问
- CORS
可以使用 CORS 来允许跨源访问。CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。
CORS 是 Cross-Origin-Resource-Sharing 的缩写,它具体的工作流程是当浏览器检测到我们发送的请求非同源时,会自动在 http 头部添加一个 origin 字段。
我们拿不到数据时因为浏览器在中间做了一层劫持。
以此分析整个跨域过程:
- 这是一次跨域请求
- 请求成功发送到服务器了
- 服务器将数据返回给了浏览器
- 服务器返回的响应头中,不曾告诉浏览器哪个域名可以访问这些数据(没有设置 Access-Control-Allow-Origin)
- 浏览器将这个数据丢弃了,抛出同源错误
这时只需要在服务的响应头中加入 Access-Control-Allow-Origin: * 即可完成跨域数据获取。
总结:
- 当浏览器发送一个跨域请求时会在 http 头部自动加上 Origin
- 需要服务配合设置响应头 Access-Control-Allow-Origin:*|Origin 即可完成跨域
- CORS 支持 GET、POST 常规请求
- CORS 支持 PUT、DELETE 等非 POST、GET 的请求,但会先发出一次预检请求。
- proxy 代理模式
核心思想:通过前端请求我们自己的后台服务,这个服务即是代理服务器,再通过代理服务器去请求另一个非同源的后台服务,因为后台之间不需要跨域,也不存在 同源策略 机制,因此可以获取到真实的数据,再将其返回给前台。
* 阅读我的其他文章:VUE006. 前端跨域代理服务器ProxyTable概述与配置 和 Nginx:多项目开发配置跨域代理 以脱离服务独自完成跨域,具体操作本文不再赘述。
- JSONP
也可以通过 JSONP (JSON with Padding) 进行跨源访问。 JSONP 是 JSON 的一种 “使用模式”,上节提到的 资源嵌入 ,其中 HTML 的 <script> 元素是同源策略的例外。利用这个元素的开放策略,网页可以得到从其他源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP 。用 JSONP 抓到的资料并非 JSON ,而是任意的 JavaScript ,用 JavaScript 直译器执行而不是用 JSON 解析器解析。如:
我们在 http://169.254.200.238:8020/jsonp/index.html 中向 http://169.254.200.238:8080/jsonp.do 发起请求。
$.get("http://169.254.200.238:8080/jsonp.do", function (data) {
console.log(data);
});
两者端口号分别为 8080 、8020 ,非同源:
使用 JSONP 请求:
<script type="text/javascript" src="http://169.254.200.238:8080/jsonp.do"></script>
Status Code: 200,可以看出 JSONP 支持且仅支持 GET 请求,不受同源限制。但浏览器抛出了语句不合法的异常:
原因是我们通过此方法请求的数据会即时被浏览器当作 JavaScript 语句执行,这点也不难看出元素 <script> 的执行机制,也就是当我们通过 URL 引入包时页面中能够立即生效的原因,也是我们必须按顺序引入包否则会报错/失效的原因。
通过上述分析其实就不难理解, JSONP 跨域的本质就是期待一个函数。如我们请求到的数据为:
callback( {"result":"success"} )
其中 {"result":"success"} 是我们想要获取的数据,浏览器会立即执行 callback 这个函数,因此我们需要:
function callback(data) {
// data为返回数据
// TODO 解析数据
}
因此 JSONP 跨域请求的关键在于:服务端要在返回的数据外包裹一层页面已经定义好的函数。
换一句话说就是,由服务器提供数据和方法名,由前端来定义这个解析数据的方法, JSONP 请求成功响应返回 callback( { someData... } ),被浏览器自动执行,就对应页面提前定义好的 function callback(data) { },其中 someData... 由 callback 中的 形参接收。
或是在 query 中告知后台前端提供的 fn 是什么:
<script type="text/javascript" src="http://localhost:12345/getJsonp?callback=show"></script>
* 使用 spring MVC 处理 jsonp 请求,可阅读文章:jsonp跨域请求详解——从繁至简
如何阻止跨源访问
- 阻止跨域写操作,只要检测请求中的一个不可推测的标记( CSRF token )即可,这个标记被称为 Cross-Site Request Forgery (CSRF) 标记。我们必须使用这个标记来阻止页面的跨站读操作。
- 阻止资源的跨站读取,需要保证该资源是不可嵌入的。阻止嵌入行为是必须的,因为嵌入资源通常向其暴露信息。
- 阻止跨站嵌入,需要确保你的资源不能通过以上列出的可嵌入资源格式使用。浏览器可能不会遵守 Content-Type 头部定义的类型。例如,如果您在HTML文档中指定 <script> 标记,则浏览器将尝试将标签内部的 HTML 解析为 JavaScript 。当您的资源不是您网站的入口点时,您还可以使用 CSRF 令牌来防止嵌入。
跨源脚本API访问
JavaScript 的 API 中,如 iframe.contentWindow 、window.parent 、window.open 和 window.opener 允许文档间直接相互引用。当两个文档的源不同时,这些引用方式将对 Window 和 Location 对象的访问添加限制,如下两节所述。
为了能让不同源中文档进行交流,可以使用 window.postMessage 。
规范:HTML Living Standard § Cross-origin objects 。
Window
允许以下对 Window 属性的跨源访问:
属性 | |
---|---|
window.closed |
只读. |
window.frames |
只读. |
window.length |
只读. |
window.location |
读/写. |
window.opener |
只读. |
window.parent |
只读. |
window.self |
只读. |
window.top |
只读. |
window.window |
只读. |
某些浏览器允许访问除上述外更多的属性。
Location
允许以下对 Location 属性的跨源访问:
属性 | |
URLUtils.href |
只写. |
某些浏览器允许访问除上述外更多的属性。
跨源数据存储访问
访问存储在浏览器中的数据,如 localStorage 和 IndexedDB(事务型数据库系统),是以源进行分割。每个源都拥有自己单独的存储控件,一个源中的 JavaScript 脚本不能对属于其它源的数据进行读写操作。
Cookies 使用不同的源定义方式。一个页面可以为本域和其父域设置 cookie ,只要是父域不是公共后缀( public suffix )即可。Firefox 和 Chrome 使用 Public Suffix List 检测一个域是否时公共后缀( public suffix )。Internet Explorer 使用其内部的方法来检测域是否是公共后缀。不管使用哪个协议( HTTP / HTTPS )或端口号,浏览器都允许给定的域一级其任何子域名( sub-domains )访问 cookie 。当你设置 cookie 时,你可以使用 Domain 、 Path 、Secure 、和 HttpOnly 标记来限定其可访问性。当你读取 cookie 时,你无法知道它时在哪里被设置的。即使您只是用安全的 https 连接,您看到的任何 cookie 都有可能是使用不安全的连接进行设置的。
参见
原始文件资料
- Author(s): Jesse Runderman
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!