从浏览器的同源策略说起
浏览器为确保资源安全,而遵循的一种策略,它限制从一个源加载的网页如何与另一个源的资源进行交互,以防止恶意网站窃取用户的敏感数据。
若没有同源策略,浏览器很容易受到XSS、CSRF等攻击。
其核心规则是: "同源"需满足以下三个条件:
- 协议相同 (如
http
与https
不同源); - 域名相同 (如
example.com
与api.example.com
不同源); - 端口相同 (如
example.com:80
与example.com:8080
不同源)。
受限制的操作 :
什么是跨域?
当请求的目标与当前页面不满足同源条件时,即发生跨域。
常见跨域场景:
当前页面URL | 目标URL | 是否跨域 | 原因 |
---|---|---|---|
http://example.com/a |
http://example.com/b |
否 | 同协议、域名、端口 |
https://example.com |
http://example.com |
是 | 协议不同(https vs http ) |
http://example.com |
http://api.example.com |
是 | 域名不同(子域名差异) |
http://example.com:80 |
http://example.com:8080 |
是 | 端口不同 |
子域名不同是否算跨域?
是的,子域名不同属于跨域。例如:
www.example.com
与api.example.com
是不同源(主域名相同但子域名不同);blog.example.com
与shop.example.com
也是跨域。
特别说明:
1.跨域限制仅存在浏览器端,服务端不存在跨域限制。 2.即使跨域,Ajax请求也可以正常发出,但响应数据不会交给开发者。
CORS
CORS概述
CORS 全称:Cross-Origin Resource Sharing (跨域资源共享),是用于控制浏览器校验跨域请求 的一套规范,服务器依照CORS规范,添加特定响应头来控制浏览器校验,大致规则如下:
- 服务器明确表示拒绝跨域 请求,或没有表示 ,则浏览器校验不通过。
- 服务器明确表示允许跨域 请求,则浏览器校验通过
备注说明:使用CORS解决跨域是最正统的方式,且要求服务器是"自己人"
常见的响应头设置:
-
Access-Control-Allow-Origin : 这个头部指定允许访问资源的域。它可以设置为特定的域名,如
https://example.com
,也可以使用通配符*
表示允许任何域访问。 -
Access-Control-Allow-Methods: 该头部用于预检请求中,告知客户端允许的实际请求方法(如GET、POST、PUT等)。这在服务器响应OPTIONS请求时非常有用。
-
Access-Control-Allow-Headers: 这个头部指定了除了简单的默认头部之外,哪些HTTP头部可以用于实际的请求。这对于非简单请求特别重要,因为它们可能包含自定义头部。
-
Access-Control-Max-Age: 预检请求的结果可以被缓存多长时间,以秒为单位。这样可以避免频繁发送OPTIONS预检请求。
-
Access-Control-Allow-Credentials : 表明是否允许发送凭据(包括Cookies和HTTP认证信息)作为请求的一部分。注意,如果设置此值为
true
,则Access-Control-Allow-Origin
不能使用通配符*
。
简单请求和复杂请求
CORS会把请求分为两类,分别是:
1.简单请求
-
方法(Method) :请求方法必须是 GET 、POST 或 HEAD。
-
头部(Headers) :请求头必须是浏览器默认的几种头部,不能包含自定义头部。具体来说,只允许以下这些头部(除非 CORS 服务器允许):
Accept
Accept-Language
Content-Language
Content-Type
(仅限text/plain
、multipart/form-data
或application/x-www-form-urlencoded
)
简记:只要不手动修改请求头,一般都能符合该规范。
2.复杂请求。
- 不是简单请求,就是复杂请求。
- 复杂请求会自动发送预检请求
CORS解决简单请求
对于简单请求 ,Access-Control-Allow-Origin
是必须的,这是跨域请求能否成功的关键。如果请求不涉及凭证,Access-Control-Allow-Origin
可以设置为 *
,允许所有源进行跨域请求;如果请求涉及凭证,则必须指定明确的源。
如果只是在使用标准的跨域请求方法(GET
, POST
, HEAD
),并且没有使用自定义的请求头部和特殊的 Content-Type
,其他响应头部(如 Access-Control-Allow-Methods
、Access-Control-Allow-Headers
等)通常是可选的,浏览器会自动允许这些请求。
大致思路:
如果要发送cookie
的话必须满足:
1.web 请求设置withCredentials
2.Access-Control-Allow-Credentials
为 true
3.Access-Control-Allow-Origin
为非 *
后面两个都是服务端配置
示例:
js
var xhr = new XMLHttpRequest();
//简单跨域请求
xhr.open('POST', 'http://localhost:8088/data1', true);
//携带 cookie
xhr.withCredentials = true;
// 对于简单请求,如何设置了不符合简单请求的请求头,就会变成复杂请求
// xhr.setRequestHeader('abc', '123');
xhr.send(JSON.stringify({ key: 'value' }))
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.responseText);
console.log(xhr.status, 'CORS Preflight successful');
} else {
console.error('CORS Preflight failed');
}
}
}
js
const http = require('http');
// 调用createServer创建http服务
const server = http.createServer((req, res) => {
const headers = {
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Credentials': 'true',
}
if(req.method === 'GET' && req.url === '/data0') {
res.writeHead(200, headers);
res.end('GET 请求成功');
return; // 终止请求处理
}
if(req.method === 'POST' && req.url === '/data1') {
let body = []
req
.on('data',(chunk)=>{
body.push(chunk)
})
.on('end',()=>{
console.log(body.toString())
res.writeHead(201, headers);
res.end('POST 请求成功');
})
return; // 终止请求处理
}
res.writeHead(404, headers);
res.end('404 Not Found');
});
server.listen(8088, () => {
console.log('服务器启动成功!')
})
CORS解决复杂请求
对于复杂请求 ,浏览器在实际发出请求之前,会先发出 预检请求(preflight request)。服务器必须对预检请求做出正确响应,才能允许实际的跨域请求。
预检请求是一个 OPTIONS 请求,浏览器会询问服务器是否允许特定的跨域请求。预检请求会携带以下信息:
Access-Control-Request-Method
:实际请求将使用的方法(例如,PUT
、DELETE
)。Access-Control-Request-Headers
:实际请求中包含的自定义请求头。Origin
:发起请求的源。
服务器必须响应以下 CORS 响应头:
-
Access-Control-Allow-Origin
:指定允许跨域的源。如果服务器允许所有源,返回*
,或者可以指定具体的源(如https://example.com
)。 -
Access-Control-Allow-Methods
:列出允许的 HTTP 方法。例如,PUT
、DELETE
、PATCH
等方法必须在此列出。 -
Access-Control-Allow-Headers
:列出允许的自定义请求头。预检请求会告知服务器实际请求将携带哪些头部,服务器必须在响应中允许这些头部。
可选:
Access-Control-Max-Age
:设置预检请求的结果可以缓存多长时间(以秒为单位)。通常设置为几小时,以减少频繁发送预检请求。
关于cookie的发送,与前面所诉一样。
示例:
js
var xhr = new XMLHttpRequest();
// 复杂跨域请求
xhr.open('PUT', 'http://localhost:3000/data', true);
xhr.setRequestHeader('Content-Type', 'application/json');
//自定义请求头,需要在Access-Control-Allow-Headers中显示声明
xhr.setRequestHeader('abc', '123');
xhr.withCredentials = true;
xhr.send(JSON.stringify({ key: 'value' }))
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.responseText,'11');
console.log(xhr.status, 'CORS Preflight successful');
} else {
console.error('CORS Preflight failed');
}
}
}
js
const http = require('http');
const server = http.createServer((req, res) => {
const headers = {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Origin': req.headers.origin || '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Header,abc',
'Access-Control-Max-Age': '86400',
};
if (req.method === 'OPTIONS') {
// 预检请求 200 201 Created 204 No Content
res.writeHead(204, headers);
console.log('预检请求');
res.end();
return; // 终止请求处理,避免继续执行后续代码
}
if (req.method === 'PUT' && req.url === '/data') {
res.writeHead(201, headers);
res.end('PUT 请求成功');
return; // 终止请求处理
}
res.writeHead(404, headers);
res.end('404 Not Found');
});
server.listen(3000, () => {
console.log('服务器正在监听端口 3000');
});
JSONP
JSONP 概述
JSONP 是利用了<script>
标签可以跨域加载脚本,且不受严格限制的特性,可以说是程序员智慧的结晶,早期一些浏览器不支持 CORS 的时,可以靠 JSONP 解决跨域。
基本流程
-
第一步: 客户端创建一个
<script>
标签,并将其src
属性设置为包含跨域请求的 URL,同时准备一个回调函数,这个回调函数用于处理返回的数据。 -
第二步: 服务端接收到请求后,将数据封装在回调函数中并返回。
-
第三步: 客户端的回调函数被调用,数据以参数的形式传入回调函数。
缺点
- 仅支持GET请求:JSONP只能通过GET方法发送请求,无法使用POST、PUT、DELETE等其他HTTP方法。
- 错误处理困难:如果请求失败,很难获取详细的错误信息,浏览器通常只会显示脚本加载失败的通用错误。
- 安全问题: JSONP从外部域加载并执行JavaScript代码,容易遭到XSS攻击。
- 性能问题 :每个JSONP请求都需要动态创建
<script>
标签,可能影响页面性能。
实现案例可以参考我之前写的文章。
postMessage
概述
postMessage
是 HTML5 的跨文档消息传递 API ,用于在不同窗口、iframe 或不同源的网页之间安全地传递消息 ,常用于跨域通信。
iframe 跨域通信
问题 :如果一个网站在 iframe
中嵌套了一个 不同源 的网页,默认情况下,它们不能直接访问彼此的 JavaScript 对象(受同源策略限制)。
解决方案 :使用 postMessage
让主页面和 iframe
之间安全通信。
示例:主页面向 iframe 发送消息
主页面(https://parent.com
):
xml
<iframe id="myFrame" src="https://child.com"></iframe>
<script>
const iframe = document.getElementById("myFrame");
// 向 iframe 发送消息
iframe.onload = function() {
iframe.contentWindow.postMessage("Hello from parent", "https://child.com");
};
// 监听 iframe 返回的消息
window.addEventListener("message", (event) => {
if (event.origin === "https://child.com") {
console.log("Received from child:", event.data);
}
});
</script>
iframe(https://child.com
)监听消息并回复
xml
<script>
window.addEventListener("message", (event) => {
if (event.origin !== "https://parent.com") return; // 只接受特定来源的消息
console.log("Received from parent:", event.data);
// 回复消息给主页面
event.source.postMessage("Hello from child", event.origin);
});
</script>
websocket
WebSocket 也是用于通信的,但它与 HTTP 不同:
-
通信方式不同:
XMLHttpRequest
和Fetch API
是基于 HTTP 的请求-响应模式,请求数据后服务器返回结果,连接随即关闭。- WebSocket 是全双工通信协议 ,可以保持连接,客户端和服务器可以 随时相互推送数据,不像 HTTP 需要客户端主动发起请求。
-
不受同源策略限制的原因:
- WebSocket 使用
ws://
或wss://
协议(安全 WebSocket)。 - 在建立 WebSocket 连接时,客户端会先通过 HTTP 发送一次
Upgrade
请求 ,要求服务器将连接升级为 WebSocket 连接(即Connection: Upgrade
)。 - 一旦连接成功,WebSocket 就不会再受同源策略的约束,因为它不再是普通的 HTTP 请求,而是一个持久化的双向通道。
- WebSocket 使用
案例:
服务端:
js
const WebSocket = require('ws');
const http = require('http');
// 1. 创建一个 HTTP 服务器
// 2. 创建一个 WebSocket 服务器
// 3. 建立一次http连接
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket server is running');
})
const wss = new WebSocket.Server({
server,
path: '/ws', // 与前端保持一致
});
wss.on('connection', (ws) => {
console.log('client connected');
ws.on('message', (message) => {
console.log('received: %s', message);
// 向客户端发送消息
ws.send('Hello from server!');
});
ws.send('Hello from server!');
})
server.listen(8080, () => {
console.log('webSocket server is running on port 8080');
})
客户端:
js
// websocket 不受同源策略的影响
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = function () {
console.log('连接成功');
ws.send('hello from client');
}
ws.onmessage = function (e) {
console.log('message from server',e.data);
}
vite反向代理
反向代理是一种 服务器代理机制,即前端开发服务器通过代理将请求转发到目标服务器。这样,浏览器只会和开发服务器通信,而不直接请求跨域的后端服务器,从而避免跨域问题。
Vite 中配置反向代理
在 Vite 项目中,可以通过 vite.config.js
文件来配置代理。
js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
// 代理 /api 路径到后端服务
'/api': {
target: 'http://localhost:5000', // 后端服务的地址
changeOrigin: true, // 如果是跨域请求,设置为 true
rewrite: (path) => path.replace(/^\/api/, ''), // 可选:重写路径,去掉 /api 前缀
},
},
},
})
总结
JSONP 通过动态 <script>
标签绕过同源限制,支持 GET 请求,安全性差。CORS 通过设置 HTTP 头部,允许跨域请求,现代浏览器广泛支持,是通信双方协商后的结果,算是真正的跨域请求解决方案。postMessage
用于不同源窗口之间安全通信,避免跨域问题。WebSocket 使用 HTTP 协议握手,连接后不受同源策略限制,适用于实时通信。Vite 反向代理 通过代理请求到目标服务器,解决开发时的跨域问题,简化配置。