1. 同源策略以及跨域的产生
- 同源策略
Same-Origin Policy
是一种浏览器安全机制,用于限制一个源origin
加载的文档或脚本如何与来自不同源的资源进行交互。 - 两个网页只有当它们的协议、主机名和端口号完全一致时,才被视为“同源”。
- 在一个网页中的脚本只能读取、写入和操作来自同一源的窗口和文档属性,而不能对不同源的资源执行这些操作,这就是跨域问题的产生。
2. 解决跨域的方法
JSONP (JSON with Padding)
:JSONP
是一种非官方跨域数据交互协议,利用<script>
标签不受同源策略限制的特点,通过动态插入脚本标签从服务器获取数据。服务器返回的数据通常是一个JavaScript
函数调用,参数即为要传递的数据。CORS (Cross-Origin Resource Sharing)
:CORS
是一种现代的、W3C
标准推荐的跨域通信机制,允许服务器明确指示哪些其他源的网站可以请求哪些资源。服务器通过在响应头中设置Access-Control-Allow-Origin
来允许特定源或者所有源的请求。- 对于简单请求,如
GET、POST
等,且不包含自定义头部或特殊请求头,浏览器会自动添加预检请求OPTIONS
或者直接附加必要的CORS
头部发送请求。 - 对于复杂请求,如带自定义头部或某些特殊的
HTTP
方法,浏览器首先发送一个预检OPTIONS
请求以询问服务器是否允许实际的请求。
- 对于简单请求,如
iframe + postMessage API
: 使用HTML5
的postMessage API
可以在嵌入的iframe
之间实现跨域通信,通过消息事件的形式发送和接收跨域数据。- 代理服务器: 在后端设置代理服务器,前端所有的请求都通过代理转发到目标服务器,这样对于前端来说,实际上仍然是同源请求。
WebSocket
协议:WebSocket
协议支持跨域连接,但同样要求服务端开启相应的CORS
设置以允许客户端发起跨域WebSocket
连接。
3. JSONP(JSON with Padding)解决跨域
- 该方式利用了浏览器允许跨域加载脚本资源的特性。它通过动态创建
<script>
标签并设置其src
属性指向需要请求的远程URL
来实现。 JSONP
利用了浏览器可以跨域加载脚本资源的“漏洞”,绕过了同源策略对Ajax
请求的限制,从而实现了跨域数据交互。但是JSONP
只能支持GET
请求,而且安全性较低,因为它依赖于不受同源策略保护的脚本注入机制。随着现代浏览器对CORS(Cross-Origin Resource Sharing)
的支持越来越广泛,JSONP
已逐渐被更安全和灵活的CORS
方案所取代。
具体步骤
- 客户端请求: 假设客户端网页想要从不同源的服务器获取数据,它客户端不是直接发起
AJAX
请求,而是动态创建一个<script>
标签,并将其src
属性指向目标服务器的URL
,并且在URL
中附带上回调函数名作为查询参数。 - 服务端响应: 服务端收到带有回调函数名的
GET
请求后,将要返回的数据包装在一个JavaScript
函数调用中,这个函数名就是客户端提供的回调函数名,当浏览器接收到这个响应时,会当作脚本执行,调用这个函数。 - 客户端在页面中预先定义好了名为
handleResponse
的全局函数。当服务器返回的数据被浏览器解析为JavaScript并执行时,handleResponse
函数会被调用,并且传递的数据作为参数传入。客户端就可以在这个回调函数内部处理来自不同源的数据了。
// 这是网页在的地址 http://example1.com/index.html
// 需要请求数据的服务器的地址 http://example2.com/getdata.php
// 1. 客户端发送请求并定义好要后端需要返回的函数参数
<script src="http://example2.com/getdata.php?callback=handleResponse"></script>
function handleResponse(data) {
// data 就是要接收的数据
console.log(data)
}
// 2. 服务器返回咱们定义的函数名的调用,并把数据当参数传入
handleResponse({name: '小明', age: 18})
// 3. 解析时,浏览器会执行返回的东西,自认就调用了我们预先设定的方法,并且我们可以从参数中得到后端返回的数据
4. CORS(Cross-Origin Resource Sharing)跨源资源共享
了解
CORS
原理得先知道简单请求与非简单请求。- 非简单请求:当发起跨域请求的方法不是
GET
、HEAD
或POST
,或者POST
请求包含非简单头部(如自定义请求头)或请求体内容类型Content-Type
不为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
。 - 简单请求:不是非简单请求就为简单请求。
- 非简单请求:当发起跨域请求的方法不是
预检机制,当发起跨域的请求为非简单请求时,浏览器会先发送一个
OPTIONS
预检请求到目标服务器,询问是否允许实际的跨域请求。预检请求中包含了Origin
头部以及其他相关CORS
头部。响应预检请求: 服务器收到预检请求后,会在响应头中添加相应的
CORS
相关头部,以决定是否允许这次跨域请求。关键的响应头包括:Access-Control-Allow-Origin
:指定允许跨域访问的源,可以是具体的源地址或表示任何源。Access-Control-Allow-Methods
:列出允许的HTTP
方法列表,比如GET
,POST
,PUT
,DELETE
等。Access-Control-Allow-Headers
:如果客户端在请求中使用了自定义头部,则这里需要列出这些头部名称,否则浏览器会拒绝请求。Access-Control-Max-Age
:设置预检请求结果的有效期,以便缓存和重用。
实际请求与响应: 如果预检请求得到服务器许可,浏览器才会发出实际的请求,并且在响应中,服务器依然需要携带合适的
CORS
响应头,特别是Access-Control-Allow-Origin
。- 对于简单请求,浏览器通常不会发送预检请求,而是会自动在请求头中添加
Origin
字段,服务器收到请求后,如果在响应头中设置了Access-Control-Allow-Origin
并且其值允许当前请求的源,那么浏览器就会认为此次跨域请求是合法的,允许JavaScript
访问响应数据,不需要设置更多的响应头参数。
- 对于简单请求,浏览器通常不会发送预检请求,而是会自动在请求头中添加
Cookie
和认证信息: 跨域请求默认情况下不会携带Cookie
和HTTP
认证信息。若要使它们随请求一起发送,服务器必须在响应头中明确声明:Access-Control-Allow-Credentials: true
:表明服务器允许客户端发送包含凭据(如Cookies
)的请求。客户端也需要在AJAX请求中设置credentials: 'include'
来传递凭据。成功跨域: 只有当服务器响应中的
CORS
相关头部符合要求时,浏览器才会将响应的数据提供给JavaScript
代码进行进一步处理。否则,浏览器会阻止这种跨域行为,并可能抛出安全相关的错误。
5. 通过前端代理服务器解决跨域问题
- 通过代理服务器解决跨域问题主要应用于前端开发环境,为了方便本地调试与后端API服务器之间的跨域请求,可以通过配置代理服务器来透明地转发请求,使得前端应用看起来像是直接与同源的代理服务器进行通信,而非直接与真正的跨域后端
API
通信。
// vue.config.js , 使用 vite 可以到 vite官网 上查看
module.exports = {
devServer: {
proxy: {
// 这里的 '/api' 是前置路径,所有发往 '/api' 的请求都会被代理
'/api': {
// 目标后端API服务器的地址
target: 'http://target-server.com',
// 是否需要改变原始请求头中的host信息
changeOrigin: true,
pathRewrite: {
// 重写路径,去掉'/api'前缀,保持请求路径与后端API的一致性
'^/api': ''
},
// 是否验证HTTPS证书(如果是https则需要设置为false)
secure: false,
// 可以添加额外的请求头
headers: {}
}
}
}
}
- 在前端代码中,我们只需像平常那样向代理路径发起请求,而不是直接向跨域的后端API发起请求。
// 这个请求将被代理到 'http://target-server.com/users'
axios.get('/api/users')
- 当前端应用在开发环境下启动时,内置的开发服务器会监听一个特定端口。当它检测到请求中有匹配到代理规则的
URL
时,会拦截这个请求,然后将请求的内容转发到目标服务器,并等待目标服务器的响应。当目标服务器响应后,代理服务器再将响应内容原封不动地转发回前端应用,此时浏览器看到的是从同源的代理服务器接收到的数据,从而避免了跨域问题。 - 通过这种方式,可以在开发阶段专注于业务逻辑,无需考虑跨域带来的影响。在生产环境中,通常服务器配置会支持CORS或者其他跨域解决方案,确保前后端间能够正常通信。
- 正向代理与反向代理
- 前端正向代理是代理客户端(如浏览器)去访问目标服务器的过程。使用正向代理服务器时,所有的网络请求都通过这个代理服务器转发到目标服务器。前端正向代理通常用来突破网络限制(如公司内网对外网的访问限制)、隐藏客户端的真实
IP
地址,或者在开发过程中解决跨域问题。 - 后端反向代理是指代理服务器代表后端服务接收客户端请求,并将请求转发到内部网络的实际服务器的过程。一般是由运维人员在服务器端配置的,比如使用
Nginx
、Apache
等作为反向代理服务器。反向代理主要用于负载均衡、提高服务器性能、隐藏真实服务器地址、简化架构、集中处理SSL/TLS
加密握手等问题,也可以用来解决前端跨域问题。
- 前端正向代理是代理客户端(如浏览器)去访问目标服务器的过程。使用正向代理服务器时,所有的网络请求都通过这个代理服务器转发到目标服务器。前端正向代理通常用来突破网络限制(如公司内网对外网的访问限制)、隐藏客户端的真实
6. iframe + postMessage API
解决跨
这种方法允许在不同源的上下文之间安全地传递数据,一般是父与子窗口之间传输,那么什么是父与子窗口呢。
- 父窗口:通常是指最初打开或载入的窗口,或者是承载其它窗口的容器窗口。在一个网页中通过
JavaScript
或者其他方式如window.open()
打开了一个新的窗口或iframe
标签加载了新的页面时,原有的那个窗口就被称作父窗口。 - 子窗口:由父窗口创建或引入的新窗口,它相对于父窗口而言具有一定的依赖关系。如果是通过
window.open()
方法打开的新窗口,那么新窗口就是子窗口,原来的窗口是父窗口。若是通过<iframe>
标签嵌入在父页面中的另一个网页,则该iframe
中的页面也可以视作子窗口。 - 父窗口与子窗口之间可以进行通信,例如通过
window.postMessage()
方法进行跨窗口的消息传递。在关闭或刷新子窗口时,有时会对父窗口的状态或行为产生影响,子窗口通常受父窗口的生命周期管理,例如当父窗口关闭时,某些情况下子窗口也会随之关闭。
- 父窗口:通常是指最初打开或载入的窗口,或者是承载其它窗口的容器窗口。在一个网页中通过
父窗口首先在子
iframe
加载完成后,通过window.postMessage()
方法向子窗口发送一条消息。
// 父窗口
window.onload = function () {
var targetOrigin = 'http://example.com' // 子iframe的源地址
window.frames[0].postMessage('Hello from parent!', targetOrigin)
}
- 子窗口通过监听
message
事件来接收消息,并对消息作出响应。
// 子窗口
window.addEventListener('message', function (event) {
// 验证发送消息的源是否可信
if (event.origin === 'http://parent-origin.com') {
console.log('Received message:', event.data)
// 可以在这里做出相应的处理
}
})
- 子窗口主动向父窗口发送消息。
// 子窗口
function sendMessageToParent() {
var parentOrigin = 'http://parent-origin.com' // 父窗口的源地址
parent.postMessage('Hello from iframe!', parentOrigin)
}
// 调用该函数发送消息
sendMessageToParent()
- 父窗监听
message
事件来接收来自子窗口的消息。
// 父窗口
window.addEventListener('message', function (event) {
// 验证发送消息的源是否可信
if (event.origin === 'http://example.com') {
console.log('Received message:', event.data)
// 可以在这里做出相应的处理
}
})
7. 使用 WebSocket 协议
WebSocket
协议支持跨域连接,但同样要求服务端开启相应的CORS
设置以允许客户端发起跨域WebSocket
连接。- 服务端
// 例子
const WebSocket = require('ws')
wss.on('headers', (headers) => {
headers['Access-Control-Allow-Origin'] = '*' // 允许任何源
// 或者
headers['Access-Control-Allow-Origin'] = 'http://example.com' // 允许特定源
});
- 客户端代码中创建
WebSocket
连接时,浏览器会自动添加Origin
头部,标识本次请求的源。服务端会检查这个Origin
头部,并基于其设置的CORS策略来决定是否允许连接。
const socket = new WebSocket('wss://example.com/ws')
socket.onopen = function(event) {
// 连接成功,可以开始发送和接收数据
}
socket.onerror = function(error) {
// 处理错误,包括可能的跨域错误
}
- 携带凭证
Cookies
的跨域,服务端还需要设置Access-Control-Allow-Credentials
响应头为true
,同时客户端在建立WebSocket
连接时也需要设置credentials
选项为true
。
// 服务端
headers['Access-Control-Allow-Credentials'] = true
// 客户端
const socket = new WebSocket('wss://example.com/ws', { credentials: 'include' }
8. 总结
一般情况下我们在开发环境使用前端代理来解决跨域问题,在生产环境下使用CORS
跨源资源共享来解决跨域问题。