什么是同源
"源"指的是协议(Protocol)、域名(Domain)和端口号(Port)三者的组合。只有当两个资源(如网页、脚本、图片等)的协议、域名和端口号都相同时,它们才被视为来自同一个源,即同源策略(Same-Origin Policy)。
一个源可以简单理解为一个网页地址(不包含 / 后面的路径和参数等)。
什么是跨域
"跨域"(Cross-Domain)是一种浏览器安全策略。两个不同源,当一个源中的脚本(如 JavaScript 代码、Ajax 请求等)尝试访问另一个源的资源时,就会产生跨域问题。跨域访问通常是不被允许的,除非使用了某些特定的机制来绕过这些限制。
需要注意的是,跨域问题仅仅只存在于浏览器中,而两台不同 ip 的服务器之间的通信问题(例如微服务之间)应该归属到网络问题,而不是跨域问题。
什么是 CORS
"跨域资源共享"(Cross-Origin Resource Sharing, CORS)就是解决跨域请求问题的一种具体机制,是一种基于 HTTP 头部的安全功能,通过发送额外的 HTTP 头部来告诉浏览器哪些跨域请求是被允许的,使得在网页中通过 AJAX 或 Fetch API 等技术从不同于当前网页源的服务器请求资源成为可能。这些资源可以是文本、图片、视频、AJAX 请求的响应结果等。
浏览器处理请求全过程
1. 发起请求
当用户在浏览器中访问一个网页时,该网页的 JavaScript 代码可能会通过 XMLHttpRequest、Fetch API 或其他类似机制发起对其他域的资源请求。
2. 解析请求的 URL 和请求头
浏览器解析请求的 URL,提取出协议(Protocol)、主机名(Host)和端口(Port)信息,这些构成了请求的 origin(源)。
3. 判断请求是否跨域
浏览器将当前页面的 origin 与请求资源的 origin 进行比较。
-
如果同源,浏览器将直接发送请求到服务器,而不会进行额外的 CORS 检查。(同源时大多数情况下,浏览器不会给请求头添加
Origin字段) -
如果不同源,则认为请求是跨域的。CORS 机制介入,并在请求头中添加
Origin字段(值是当前页面的源)。
4. 跨域请求
-
如果请求是简单请求,浏览器将直接发送请求。
- 简单请求:使用 GET、HEAD、POST 方法,且 Content-Type 字段的值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded
-
如果是非简单请求,浏览器会先发送一个 OPTIONS 请求的预检请求(Preflight Request)到服务器。这个预检请求包含了请求将要使用的 HTTP 方法和头部信息(包括
Origin字段),询问服务器是否允许这样的跨域请求。- 非简单请求:使用了 PUT、DELETE 方法,或者 Content-Type 字段的值不是上述三种之一
5. CORS 机制介入
服务器收到请求,根据自身的 CORS 配置,给响应头添加信息,如 Access-Control-Allow-Origin、Access-Control-Allow-Methods 等。如果服务器没配置 CORS,则不会在响应头中添加这些内容。
服务器处理完请求后,将 HTTP 响应发送给浏览器。
6. 浏览器接收响应
浏览器接收到响应后,首先检查响应头。检测响应中是否包含 CORS 相关的头部,或者 响应头中 Access-Control-Allow-Origin 的值与 Origin 的值是否匹配。
-
如果匹配,则代表服务器允许该跨域操作,浏览器正常接收响应。
-
如果不匹配,浏览器不会将响应的内容暴露给前端 JavaScript 代码。非简单请求时浏览器会阻止实际请求的发送。
7. 特殊说明
对于 <script>、<img>、<link rel="stylesheet"> 等标签,它们的 src 属性在发起 HTTP 请求时,浏览器通常不会应用 CORS 策略,也就是说他们大多数时候不受跨域请求的限制。
有哪些行为会受同源策略影响
Ajax 请求
想象一个场景,在源 A 中使用 Ajax 发起一个请求去访问源 B 时,如果源 B 没有配置 CORS 或 CORS 策略表示拒绝,浏览器会有以下 2 种处理方式:
-
简单请求:
该请求还是会正常发送的,且服务器是会进行处理的,只是在响应阶段会被浏览器阻止。
-
非简单请求:
浏览器会先发送一个 OPTIONS 请求(预检请求)到服务器,询问服务器是否接受后续的实际请求。如果服务器表示不允许该跨域请求,浏览器会正常收到 OPTIONS 请求的响应,并根据响应中的 CORS 相关头部来决定是否阻止实际请求的发送。
DOM 和 Javascript 对象交互
JavaScript 无法直接操作或访问来自不同源的 DOM 元素和 Javascript 对象,包括尝试读取或修改 iframe(如果其内容与父页面不同源)中的 DOM 元素。
Cookie、LocalStorage、IndexDB 读写
无法读写其他域的 Cookie、LocalStorage、IndexDB 等 Web 存储机制。
CSS 中的 @font-face
CSS 的 @font-face 规则允许你定义自己的字体,并指定字体文件的 URL,会受到同源策略影响。
有哪些行为不受同源策略影响
<script> 标签
<script> 标签用于加载并执行 JavaScript 代码。它可以从不同的源(域名)加载脚本文件,这是 JSONP 的基础,也是 Web 开发中非常常见的做法,用于加载库、框架或应用程序的脚本。
但是需要注意的是跨域的脚本不能直接访问或修改当前页面的敏感数据。
<link> 标签
<link> 标签(对于CSS)通常与 rel="stylesheet" 属性一起使用,用于加载外部 CSS 样式表。
<iframe> 标签
<iframe> 元素创建了一个内联框架,该框架可以包含另一个 HTML 文档。这允许你在当前页面中嵌入另一个源的内容。<iframe> 的 src 属性指定了要加载的文档的 URL,该 URL 可以是跨域的。
<audio> 和 <video> 标签
这些 HTML5 标签用于在网页上嵌入音频和视频内容。它们的 src 属性可以设置为指向跨域资源的 URL,以加载音频或视频文件。
CSS 背景图像
在 CSS 中,你可以使用 background-image 属性为元素设置背景图像。这个属性的值可以是一个 URL,指向跨域的图片资源。
SVG 中的 <image> 元素
在 SVG(可缩放矢量图形)中,<image> 元素允许你嵌入位图图像。这个元素的 href 或 xlink:href 属性可以设置为指向跨域的图片 URL。
跨域问题解决方法
一、前端解决方案
JSONP(JSON with Padding)
JSONP 跨域攻击需要后端配合,它利用 <script> 标签不受同源策略限制的特性,通过动态创建 <script> 标签,并在其 src 属性中指定一个跨域的 URL,来实现跨域请求数据。这个 URL 指向的后端接口需要后端特别处理,以返回符合 JSONP 格式的响应。
例如:<script src="https://example.com/data?callback=myFunction"></script>
-
https://example.com/data是后端接口的地址,callback是一个必须参数,没有callback参数,后端服务器就无法知道应该将返回的数据传递给哪个函数 -
myFunction是原 web 应用的 JavaScript 代码中的一个函数名称
所以这行代码的意思是:
-
调用
https://example.com/data接口,并获取返回数据; -
后端将数据进行封装,封装后的数据看起来会是一个有效的 JavaScript 代码片段,比如
myFunction({"key":"value"}); -
后端将封装后的数据(即 JavaScript 调用表达式)作为 HTTP 响应体返回给前端;
-
由于这个响应是通过
<script>标签加载的,因此当浏览器接收到这个响应时,它会执行其中的 JavaScript 代码; -
所以最终浏览器会执行
myFunction({"key":"value"})函数。
document.domain + iframe
原理:此方法适用于主域相同但子域不同的跨域场景。通过设置两个页面的 document.domain 为同一个值,使得来自不同子域的页面能够被视为同源,从而绕过同源策略的限制,可以实现两个页面之间的脚本互访。
限制:只能在主域相同的情况下使用。
示例:
http://example.com/parent.html(父页面)
http://child.example.com/child.html(iframe中的页面)
<!--父页面-->
<script>
document.domain = 'example.com';
function parentFunction() {
alert('Called from parent');
}
window.onload = function() {
var iframe = document.getElementById('childFrame');
var childWindow = iframe.contentWindow;
// 现在可以调用iframe中的函数
childWindow.childFunction();
};
</script>
<iframe id="childFrame" src="http://child.example.com/child.html"></iframe>
<!--子页面-->
<script>
document.domain = 'example.com';
function childFunction() {
alert('Called from child');
// 调用父页面中的函数
parent.parentFunction();
}
</script>
window.name + iframe
原理:window.name 属性是全局的,并且可以在不同页面(即使不同域)之间保持。因此,可以通过设置 window.name 来传递跨域数据。
实现:通常涉及两个页面,一个用于设置 window.name (包含数据的页面),另一个用于读取 window.name (接收数据的页面)。这两个页面需要通过 iframe 嵌入。
postMessage
HTML5 引入的 window.postMessage 方法允许跨源通信。这个方法可以在两个不同源的窗口(或 iframe)之间安全地传递消息。
语法:
-
发送:
window.postMessage(message, targetOrigin, [transfer])-
message:要发送的数据
-
targetOrigin:是一个 URL,指定目标源,也可以设置为
"*",则表示不限制域名 -
[transfer]:(可选)是一串和
message同时传递的Transferable对象。
-
-
接收:
-
event.origin:源数据的值
-
event.source:发送消息的窗口
-
event.data:发送的数据
-
示例:
<!--父页面 https://your-parent-page-origin.com/parent.html-->
<!DOCTYPE html>
<html>
<body>
<iframe id="childFrame" src="https://example.com/child.html" style="width: 100%; height: 200px;"></iframe>
<script>
var iframe = document.getElementById('childFrame');
iframe.onload = function() {
iframe.contentWindow.postMessage('Hello, child!', 'https://example.com');
};
</script>
</body>
</html>
<!--子页面 https://example.com/child.html-->
<!DOCTYPE html>
<html>
<body>
<script>
window.addEventListener('message', function(event) {
if (event.origin !== 'http://your-parent-page-origin.com') { // 替换为你的父页面源
return; // 如果不是来自预期的源,则忽略消息
}
console.log('Received message:', event.data);
// 在这里处理接收到的消息
});
</script>
</body>
</html>
-
iframe.contentWindow.postMessage:当你需要从父页面向嵌入的<iframe>子页面发送消息时使用。 -
window.postMessage:当你需要在同源的不同窗口之间或者从一个窗口向其打开的另一个窗口发送消息时使用。注意:使用 postMessage 并不是没有限制的,使用 postMessage 进行通讯的页面之间必须有某种关联,页面要么是父子关系(如通过 iframe 嵌入),要么是通过 window.open() 打开的新窗口。
服务器代理 Nginx
假设你有以下配置:
-
Nginx 配置(位于
192.168.1.1):location ~ /API/ { add_header 'Access-Control-Allow-Origin' 'http://frontend.example.com'; # 其他 CORS 相关头部... } -
后端 API(位于
192.168.1.2): 提供/API/路径下的服务。
情景 1:合法请求
-
用户访问
http://frontend.example.com。 -
前端应用通过 AJAX 请求向
http://backend.example.com/API/发起请求。 -
浏览器在请求头中包含
Origin: http://frontend.example.com。 -
Nginx 收到请求并检查
Origin头,发现它匹配Access-Control-Allow-Origin的设置,于是允许请求继续进行。
情景 2:非法请求(钓鱼网站)
-
用户访问钓鱼网站
http://phishing-site.com。 -
钓鱼网站尝试通过其页面中的脚本向
http://backend.example.com/API/发起请求。 -
浏览器在请求头中包含
Origin: http://phishing-site.com。 -
Nginx 收到请求并检查
Origin头,发现它不匹配Access-Control-Allow-Origin的设置,因此返回响应头告诉浏览器拒绝该请求。 -
浏览器接收到响应后,不会处理响应数据,并向开发者控制台报告跨域错误。
总结:
综上所述,设置 add_header Access-Control-Allow-Origin "http://frontend.example.com" 的意义在于确保只有来自指定源(即 http://frontend.example.com)的请求才能成功访问后端 API,从而有效地防止了来自其他来源(如钓鱼网站)的非法跨域请求。这样做不仅遵循了 CORS 标准,还增强了应用程序的安全性。即 Nginx 实际上是在为后端 API 提供 CORS 控制。
这里容易产生混淆的一个点是:如果你的意图是允许来自特定 IP 地址的请求(即根据 IP 地址控制访问),那么你需要使用其他机制,比如 Nginx 的 allow 和 deny 指令
Nginx 在将请求转发到后端服务器时,并不会遇到跨域问题,因为 Nginx 和后端服务器之间的通信是在服务器端进行的,不受浏览器同源策略的限制。Nginx 只是简单地将请求转发给后端服务器,并将后端服务器的响应返回给浏览器。
当在 Nginx 中使用 add_header 'Access-Control-Allow-Origin' 'http://frontend.example.com'; 这样的指令,实际上是在告诉 Nginx 服务器,在响应中添加一个名为 Access-Control-Allow-Origin 的响应头,并将其值设为 http://frontend.example.com。浏览器接收到响应后,会检查这个响应头,并根据其值决定是否允许前端 JavaScript 代码访问该响应。
如果后端服务设置了跨域请求的 Access-Control-Allow-Origin 为
"*",Nginx 通过 add_header Access-Control-Allow-Origin 设置了http://frontend.example.com,那么当 url 为http://test.com这个域名下的网页通过 AJAX 等方式尝试向由 Nginx 服务的某个 API 端点发送 HTTP 请求,会发生跨域吗?
会发生跨域,因为 Nginx 的 CORS 设置将优先于后端服务的设置。如果 Nginx 设置了 CORS 相关的内容,那么后端服务器就不需要再重新设置了。
二、后端解决方案
修改服务器配置
原理:在服务器端设置响应头,允许来自不同源的请求。例如,在 CORS 中,服务器需要设置 Access-Control-Allow-Origin 等响应头来允许跨域请求。
实现:根据具体的服务器软件和框架进行配置。
三、其他解决方案
使用 WebSocket
原理:WebSocket 协议允许通过单个 TCP 连接进行全双工通讯,在客户端和服务器之间建立持久的连接。由于 WebSocket 协议是基于 TCP 的,因此不受同源策略的限制。
适用场景:需要实时数据交互的跨域场景。
前端示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Client</title>
<script>
window.onload = function() {
var ws = new WebSocket('ws://your-server-url/websocket');
ws.onopen = function(event) {
console.log('Connection opened');
ws.send('Hello Server!');
};
ws.onmessage = function(event) {
console.log('Received:', event.data);
};
ws.onclose = function(event) {
console.log('Connection closed');
};
ws.onerror = function(error) {
console.error('WebSocket Error: ', error);
};
};
</script>
</head>
<body>
<h1>WebSocket Client</h1>
</body>
</html>
ws:// 是 WebSocket 协议的固定写法,用于指定 WebSocket 通信的 URL 应该使用未加密的 WebSocket 连接。这是与 HTTP 协议的 http:// 类似的一种协议标识符。
除了 ws:// 之外,还有一个加密的 WebSocket 协议标识符 wss://,它类似于 HTTPS 的 https://,表示 WebSocket 连接将通过 TLS/SSL 加密。
设置浏览器代理
原理:通过修改浏览器的代理设置,将所有请求都通过代理服务器转发,从而绕过同源策略的限制。
实现:在浏览器设置中配置代理服务器。
使用第三方库
一些第三方库(如 axios、fetch 等)提供了对 CORS 的支持,可以在前端代码中直接使用这些库来发起跨域请求。
ajax 请求只能访问同一源下的资源,不能直接发起跨域请求,axios、fetch 等内置了对 CORS 的支持,可以发起跨域请求。但是要注意的是,可以发起跨域请求并不代表就能请求成功,并不代表能解决跨域问题,如果浏览器的同源策略阻止了跨域请求,那么 axios 发起的跨域请求将会失败。
CSRF
跨站请求伪造,也叫 XSRF,是一种通过伪装用户请求,在用户不知情的情况下以用户的身份执行恶意操作的网络攻击手段。
CSRF 攻击过程
-
用户登录网站A:用户User在浏览器中打开网站A,并成功登录。在这个过程中,网站A会在用户的浏览器中设置一个或多个cookie,用于跟踪用户的会话状态。
-
用户访问网站B:在用户User的浏览器会话中,他/她随后访问了网站B(可能是通过点击一个链接、查看一封电子邮件中的图片或执行其他操作)。
-
网站B构造CSRF攻击:网站B包含了一些恶意代码(如HTML表单或JavaScript脚本),这些代码被设计为向网站A发送HTTP请求。重要的是,这些请求的目标URL是网站A的某个敏感操作(如转账、更改密码等)。
-
浏览器发送请求:当用户User的浏览器解析并执行网站B上的恶意代码时,它会根据代码中的指令向网站A发送HTTP请求。由于这个请求是在用户C的浏览器会话中发起的,并且用户C之前已经登录了网站A,因此浏览器会自动将网站A的cookie附加到这个请求中。
-
网站A处理请求:网站A接收到请求后,会检查请求头中的cookie以验证用户的身份。由于请求中包含了有效的cookie,网站A无法区分这个请求是来自用户C的合法请求还是来自网站B的CSRF攻击。因此,网站A会根据cookie信息以用户C的权限处理该请求。
这里有 2 个疑问:
- 既然有 CORS 策略,那么网站B中的代码向网站A发起请求就会被浏览器拦截,那为什么还会发生 CSRF 呢?
因为 CSRF 攻击通常不是使用 Ajax 发起攻击,通常是通过 HTML 表单提交、图片标签的 src 属性、iframe 的 src 属性等自动地发起 HTTP 请求,这些 HTML 元素触发的请求不受 CORS 策略的限制,因为它们是由用户的浏览器根据 HTML 规范自动处理的。
- cookie 是会受到跨域影响的,那么网站B是如何向网站A发送带有A的 cookie 的请求的呢?
首先理解 cookie 的发送机制:当浏览器向服务器发送 HTTP 请求时,如果请求的资源(URL)与某个 cookie 的域相匹配,那么浏览器会自动将该 cookie 附加到请求头中(通常是Cookie头)。这是浏览器自动完成的,无需任何脚本干预。
cookie 并不是被 "发送" 到网站B的,也不是由网站 B"控制" 的。相反,cookie 是存储在用户User的浏览器中的,并且当浏览器向与 cookie 域相匹配的服务器发送请求时,它会自动附加这些 cookie。这就是 CSRF 攻击能够利用用户已登录状态的 cookie 来执行敏感操作的原因。
CSRF 攻击主要是用来做增删改操作,很少用来做查询。因为查询结果会被浏览器的同源策略影响,攻击者通常无法通过这种方式直接获取查询结果。即使查询请求被发送到了目标服务器,响应结果也通常会被用户的浏览器拦截,并且不会直接显示在攻击者控制的页面上。
如何有效防止 CSRF 攻击
使用 Token
在每次用户会话中,服务器向客户端发送一个随机生成的 Token(令牌),客户端在后续请求中需要将这个 Token 包含在请求中。服务器在收到请求后,会验证 Token 的有效性。
为什么 Token 可以防止 CSRF 攻击?
Token 是由服务器在每次用户会话中随机生成的,因此每次会话的 Token 都是唯一的。
由于 Token 的随机性,恶意网站几乎无法预测或猜测到当前用户会话中的有效 Token。
虽然浏览器会自动在跨站请求中携带同源的 Cookie,但它不会自动携带 Token(除非 Token 被存储在 Cookie 中且未设置 HttpOnly 属性,但这种情况并不推荐)。因此,恶意网站无法直接获取到用户会话中的有效 Token。
验证 HTTP Referer 或 Origin 头部
HTTP 请求头中的 Referer 或 Origin 字段记录了请求的来源地址。通过验证这些字段,可以判断请求是否来自受信任的源。
服务器在收到请求后,检查 Referer 或 Origin 字段的值,确认其是否与预期的来源地址匹配。
需要注意的是,Referer 字段可以被伪造或禁用,因此更推荐使用 Origin 字段进行验证。
使用 HTTPS
HTTPS 并不直接防止 CSRF 攻击,但它能加强数据传输的安全性,防止中间人攻击。