跨域问题的由来
跨域问题的根源都是浏览器的同源策略的限制,它用于限制网站或者加载的脚本与其他网站进行资源交互,这样能够有效帮助阻拦恶意文件,保护用户的安全。
同源策略规定,只有两个页面具有同样的协议 protocol
、主机名 host
、端口号 port
才称为同源能够直接进行交互,三者有任意一种不同就会造成跨域问题
其实即使跨域了,ajax请求也并未被拦截而是成功发送到服务端,服务端正常处理请求后返回资源,浏览器接收到资源后一看,当前网页与请求地址不同源,拒绝将服务端返回的资源传递给代码。所以跨域问题是发生在浏览器的,与网络请求没有关系
同源策略的限制
如果两个网页不同源则
- 无法读取
cookie
、localstorage
、IndexedDB
- 无法获取或操作另一个源的
DOM
- 无法发送
ajax
请求
跨域的解决办法
一、 JSONP跨域
JSONP
是服务器与客户端通信的一种简单方法,其主要原理就是利用 script
标签的 src
属性能过跨域访问
简单来说就是通过在页面添加一个 script
标签向服务端发送请求,服务端接收请求时,将数据放到指定的回调函数参数中传回来
但 jsonp
只支持 get
请求( script
标签就是 get
请求)并不支持 post
、 put
等其他请求方式
// 服务端配置
// 用express简单搭建一个服务器
const express = require('express');
const app = express();
app.get('/', (req, res) => {
// 从query中拿取回调函数名,以及相应数据
const callback = req.query.callback;
const name = req.query.name || "hello world";
res.send(`${callback}('${name}')`);
res.end();
})
// 监听3000端口
app.listen(3000, () => {
console.log("listen 3000");
});
<!-- 前端代码 -->
<h1 id="test"></h1>
<script>
function sendAjax(name) {
// 定义callback名(可以随意)
const callback = "doSomething"
// 在全局定义这个函数
window[callback] = function (data) {
document.querySelector('#test').innerHTML = data;
// 用完后移除
delete window[callback];
}
// 创建一个script标签,设置好src后添加到head中
const script = document.createElement('script');
script.src = `http://localhost:3000?name=${name}&callback=${callback}`;
document.head.appendChild(script);
// 移除script标签
document.head.removeChild(script);
}
sendAjax("123");
</script>
二、cors 跨域
CORS
是基于http1.1
的一种跨域解决方案,它的全称是Cross-Origin Resource Sharing,跨域资源共享。
总体思路就是:如果浏览器要跨域访问服务器的资源,需要得到服务器的许可。
针对不同的请求,cors规定了几种不同的交互模式
- 简单请求
- 复杂请求(需要先发送预检请求)
简单请求
简单请求的判断
- 请求方法为以下的一种
- get
- post
- head
- 请求头仅包含安全字段,常见安全字段
Accept
Accept-Language
Content-Language
Content-Type
(仅能为以下值text/plain
、multipart/form-data
、application/x-www-form-urlencoded
)
满足以上条件则视为简单请求。
简单请求交互
- 在请求头中加入
origin
字段
例如在页面 http://127.0.0.1:5500
有以下代码造成了跨域
// 端口号不同造成跨域问题
fetch("http://127.0.0.1:3000");
<!-- 请求头的部分信息 -->
Connection: keep-alive
Host: localhost:3000
...
Origin: http://127.0.0.1:5500
Referer: http://127.0.0.1:5500/index.html
origin
字段会告诉服务器是哪个源地址在请求服务器
- 服务器响应头中包含
Access-Control-Allow-Origin
字段
当服务器接收到请求后,对请求头中的origin
进行判断后,如果允许访问,需要在响应头中添加Access-Control-Allow-Origin
字段
值可以是
- * : 表示允许任何源访问
- 具体的源 : 例如
http://127.0.0.1:5500
,表示仅允许当前源访问
复杂请求交互
当浏览器认为这不是一个简单请求时,会按照下列步骤进行请求
- 浏览器先发送预检请求(opption),询问服务器是否允许
- 服务器进行判断,允许访问
- 浏览器发送真实请求
- 服务器完成真实响应
例如在页面 http://127.0.0.1:5500
有以下代码造成了跨域
fetch("http://localhost:3000/test", {
// post方法
method: "post",
// 自定义请求头
headers: {
name: "Nt",
"content-type": "application/json"
},
// 请求体
body: JSON.stringify({name:"CORS"}),
})
- 浏览器发送预检请求
预检请求体(方法为 OPTIONS )
Access-Control-Request-Headers: content-type,name
Access-Control-Request-Method: POST
Connection: keep-alive
Host: localhost:3000
...
Origin: http://127.0.0.1:5500
Referer: http://127.0.0.1:5500/index.html
预检请求它的目的是询问服务器是否允许后续的真实请求,它包含了后面真实请求的相关信息,所有预检请求有以下特征
- 请求方法为
OPTIONS
- 没有请求体
- 请求头中包含
origin
:发出请求的源Access-Control-Request-Headers
:后续真实请求会改动的请求头Access-Control-Request-Method
:后续真实请求的方法
- 服务器允许 服务器收到预检请求后,可以检查预检请求中的信息,如果允许访问,需要做如下响应
...
Access-Control-Allow-Headers: content-type,name
Access-Control-Allow-Method: POST
Access-Control-Allow-Origin: http://127.0.0.1:5500
...
对于预检请求,服务度不需要响应任何消息体,只需在响应头中添加
Access-Control-Allow-Headers
:允许真实请求改动的请求头Access-Control-Allow-Method
:允许真实请求的请求方法Access-Control-Allow-Origin
:与简单请求相同表示允许访问的源
- 浏览器发送真实请求,服务端响应真实请求
预检请求后,浏览器就会发送真实请求,后续的请求处理与简单请求相同
// 手动实现简单cors
// 这里还是使用express框架
// 定义一个中间件
app.use((req, res, next) => {
// 如果请求方法是options那该请求就是复杂请求
if (req.method.toLocaleLowerCase() === "options") {
// 读取请求头中的相关信息
const nextHead = req.headers['access-control-request-headers'];
const nextMethod = req.headers['access-control-request-method'];
// 拿到信息后可以进行一些判断,这里我就直接放行了
res.setHeader('Access-Control-Allow-Headers', nextHead);
res.setHeader('Access-Control-Allow-Method', nextMethod);
}
// 简单请求
// Origin字段无论简单请求还是复杂都需要配置
// 从请求头中获取origin
const nextOrigin = req.headers.origin;
// 这里我还是直接放行了
res.setHeader('Access-Control-Allow-Origin', nextOrigin);
next();
})
三、websocket 跨域
WebSocket
是 HTML5
中新增的协议,支持持久性连接,解决了 HTTP协议
通信只能由客户端发起的缺陷。
如何建立websocket连接
客户端需通过http请求与WebSocket服务端协商升级协议,协议升级完成后,后续的数据交换遵照WebSocket的协议。 请求头中会一些特征字段
<!-- 表示协议需要升级 -->
Connection: Upgrade
<!-- 请求扩展 -->
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
<!-- 与后续服务端响应首部Sec-WebSocket-Accept配套,提供安全保护 -->
Sec-WebSocket-Key: A+C1VOMnuprTFHwx7R0bUA==
<!-- websocket版本 -->
Sec-WebSocket-Version: 13
<!-- 表示要升级到websocket协议 -->
Upgrade: websocket
服务端接收到返回
Connection: Upgrade
<!-- 与Sec-WebSocket-Key配套 -->
Sec-WebSocket-Accept: 2zJTRh8A8UEc9ZttNOE9ilY73gg=
Upgrade: websocket
至此协议升级成功后续请求通过websocket发送 websocket协议不存在跨域问题,不需要任何额外的配置就能进行跨域访问
四、nginx代理
Nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,从而实现实现跨域访问。