什么是同源策略
同源策略 是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互,它能帮助阻隔恶意文档,减少可能被攻击的媒介。
MDN上有详细解释, 如果两个 URL 的协议、端口(如果有指定的话)和主机都相同的话,则这两个 URL 是同源 的。 比如:跟URL http://store.company.com/dir/page.html
的源进行对比,这个源默认端口是80。
浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作 。 同源策略控制不同源之间的交互,例如在使用 XMLHttpRequest
或 <img>
标签时则会受到同源策略的约束。这些交互通常分为三类:
1、跨源写操作 (Cross-origin writes)一般是被允许的,如链接links
、重定向
以及表单提交(如form表单的提交)
2、跨源资源嵌入 (Cross-origin embedding)一般是被允许的,如 img
、script
标签
3、跨源读操作 (Cross-origin reads)一般是不被允许的,如我们的AJAX接口请求
等
什么是跨域问题
浏览器出于安全考虑,对于同源的请求通过,对于不是同源 的请求会限制,这就是浏览器的 同源策略,对于不满足浏览器浏览器同源策略出现的开发问题,就是跨域问题。
说得更直白一点,我们通过一个url访问一个页面,这个页面的url叫做页面源 ,在这个页面中会有很多的请求,比如请求css,js,图片等等,还有使用xhr或者fetch去请求一些动态数据,这些请求的都url地址叫做目标源 ,当页面源和目标源的协议、主机、端口不一致,就会出现跨域问题。本文重点关注使用AJAX发请求遇到的跨域问题。
注意:当跨域请求时,浏览器同样会发出请求,服务器也同样会响应,但是当浏览器接受到服务器的响应时会去校验,校验不通过,就会出现跨域报错。
比如在百度页面里请求淘宝,跨域报错了,但是淘宝服务器是成功响应了的。
解决方案
CORS
是W3C的标准,是AJAX跨域请求的根本解决方式。在 Node代理 和 Nginx反向代理中一些情况也离不开CORS
MDN上面 跨源资源共享(CORS)和阮一峰老师的 跨域资源共享 CORS 详解 做了详细的解释 ,CORS是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。它使浏览器通过对跨域请求的校验。使用这种方法,关键在于服务器。
区分简单请求和非简单请求(预检请求)
CORS把请求分成两种:简单请求和预检请求。
满足下面所有条件的是简单请求:
1、请求方法是:get,head,post;
3、请求头信息不超过以下几个字段 Accept,Accept-Language,Content-Language,Content-Type
, 不要去改其他请求头字段;
2、Content-type类型只能是: text/plain
, multipart/form-data
,application/x-www-form-urlencoded
;
4、如果请求是使用 XMLHttpRequest
对象发出的,在返回的 XMLHttpRequest.upload
对象属性上没有注册任何事件监听器;也就是说,给定一个 XMLHttpRequest
实例 xhr
,没有调用 xhr.upload.addEventListener()
,以监听该上传请求。
5、请求中没有使用 ReadableStream
对象。
简单请求以外的就是非简单请求。
例如:
在淘宝页面请求百度页面,使用put方法,非简单请求,method:put + preflight
在淘宝页面请求百度页面,修改请求头,非简单请求,method:get + preflight
在淘宝页面请求百度页面,修改content-type,非简单请求,method:post + preflight
浏览器发送简单请求
浏览器发现跨域是个简单请求,会带上一个请求头Origin
,Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
服务器处理简单请求
要解决跨域,服务器只需要设置对应到响应头:
Access-Control-Allow-Origin
必须设置的字段 ,可以设置*
,允许所有源访问,或者设置明确允许的源,但是就不能发送cookie了,需要明确设置为允许通过的源
Access-Control-Allow-Credentials
在跨域请求下,浏览器是不带 Cookie 的。但是客户端可以通过设置 withCredentials
来进行传递 Cookie
js
// xhr
const xhr = new XHRHttpRequest();
xhr.withCredentials = true;
// Axios
axios.default.withCredentials = true;
// fetch
fetch(url, {credentials: 'include'});
服务端设置
js
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:非 *****
服务端实现测试:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CORS简单请求</title>
</head>
<body>
<script>
fetch("http://localhost:2023/cross");
</script>
</body>
</html>
js
const Koa = require("koa");
const koaRouter = require("@koa/router");
const app = new Koa();
const router = new koaRouter();
router.get("/cross", (ctx, next) => {
ctx.body = {
message: "跨域简单get请求成功"
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa服务器启动"));
把html用本地或者live server方式打开,然后请求koa起的服务器地址,会出现跨域问题,不同源
给响应头设置Access-Control-Allow-Origin:* 就请求成功了,
js
const Koa = require("koa");
const koaRouter = require("@koa/router");
const app = new Koa();
const router = new koaRouter();
app.use(async (ctx, next) => {
// ctx.set("Access-Control-Allow-Origin", "*");
// 或者
ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
ctx.set("Access-Control-Allow-Credentials", true);
await next();
});
router.get("/cross", (ctx, next) => {
ctx.body = {
message: "跨域简单get请求成功",
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa服务器启动"));
浏览器发送非简单请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight
):
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的Ajax请求,否则就报错。
"预检"请求用的请求方法是OPTIONS
,预检请求中,请求头字段:
Origin
,必带的字段;
Access-Control-Request-Method
,必带的字段,用来列出浏览器的CORS请求用到的HTTP方法;
Access-Control-Request-Headers
:是一个逗号分隔的字符串,实际请求将携带自定义请求头字段
例如:
put请求,请求头中没有Access-Control-Request-Headers
get请求,header中加自定义字段,请求头中有Access-Control-Request-Method
、Access-Control-Request-Headers
:
put请求,修改content-type,请求头中有Access-Control-Request-Method
、Access-Control-Request-Headers
:
服务器处理非简单请求
响应头字段:
Access-Control-Allow-Origin
,必须设置的字段,允许通过预检的源
Access-Control-Request-Method,
也是必须设置的字段,服务器允许请求头头中携带字段
Access-Control-Request-Headers
,服务器将接受后续的实际请求方法
Access-Control-Max-Age,
减少预请求的次数,需要包含在预请求的响应头中,指定在该时间内预请求验证有效,不必每次都进行预请求,它的单位是 s
。如 Access-Control-Max-Age: 86400
,即有效期为1天
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CORS简单请求</title>
</head>
<body>
<script>
fetch("http://localhost:2023/cross", {
method: "put",
headers: {
"content-type": "application/json",
"my-header": "123",
},
});
</script>
</body>
</html>
</script>
Node原生实现:
js
const Koa = require("koa");
const koaRouter = require("@koa/router");
const app = new Koa();
const router = new koaRouter();
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
ctx.set("Access-Control-Allow-Headers", "Content-type,My-header");
ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
ctx.set("Access-Control-Allow-Credentials", true);
ctx.set("Access-Control-Max-Age", 86400);
await next();
});
router.put("/cross", (ctx, next) => {
ctx.body = {
message: "跨域put请求成功",
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa服务器启动"));
koa中间件实现
js
const Koa = require("koa");
const koaRouter = require("@koa/router");
const cors = require("koa2-cors");
const app = new Koa();
const router = new koaRouter();
app.use(
cors({
origin: function (ctx) {
// 允许的源
return ctx.headers.origin;
},
// 指定本次预检请求的有效期,单位为秒
maxAge: 500000,
// 是否允许发送 Cookie
credentials: true,
// 设置所允许的 HTTP 请求方法
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
// 设置服务器支持的所有头信息字段
allowHeaders: ["Content-Type", "My-header"],
})
);
router.put("/cross", (ctx, next) => {
ctx.body = {
message: "跨域put请求成功",
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa服务器启动"));
JSONP
出现的早,兼容性好(兼容低版本IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。
缺点是只支持 GET
请求,不支持 POST
请求。
跨源资源嵌入 (Cross-origin embedding)一般是被允许的,动态创建 <script>
脚本标签,通过跨域脚本嵌入不受同源策略限制的方法实现请求第三方服务器数据内容。除了适用于 <script>
脚本标签,HTML 中包含 src
和 href
属性的标签均不受同源策略限制。
所以JSONP的做法是, 把前端定义好的一个函数名,放在src路径上,当作参数传递给服务端,然后服务端拿到这个函数名, 拼成一个函数调用字符串返回前端,并把响应结果当作参数传递给前端:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSONP请求</title>
</head>
<body>
<script>
// 动态创建脚本标签
const script = document.createElement("script");
// 设置接口地址
script.src = "http://localhost:2023/cross?callback=jsonpCallback";
// 插入页面
document.body.appendChild(script);
// 通过定义回调函数接收响应数据
function jsonpCallback(res) {
console.log(res);
// todo
}
</script>
</body>
</html>
服务端实现
js
const Koa = require("koa");
const koaRouter = require("@koa/router");
const app = new Koa();
const router = new koaRouter();
router.get("/cross", (ctx, next) => {
// 拿到前端传递的函数名
const { callback } = ctx.query;
// 返回一个函数调用的字符串
ctx.body = `${callback}(${JSON.stringify({
msg: "jsonp跨域请求成功",
code: 200,
})})`;
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa服务器启动"));
Node代理
CORS
和 JSONP
都需要服务器的配合,CORS
需要服务器设置响应头,JSONP
需要服务器返回一段函数调用的Js代码,使用Node代理就不需要后端的配合。
代理服务器设置CORS
使用koa起一个目标服务器
js
// 目标服务器 target.js
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const app = new Koa();
const router = new KoaRouter({ prefix: "/cross" });
router.get("/", (ctx) => {
ctx.body = {
msg: "node代理请求成功",
code: 200,
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa启动"));
客户端请求,使用 live server或者本地打开
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Node代理</title>
</head>
<body>
<button id="btn">Node代理请求</button>
<script>
const btnEL = document.querySelector("#btn");
btnEL.onclick = function () {
fetch("http://localhost:2023/cross");
};
</script>
</body>
</html>
此时必然出现跨域问题
再使用express
起一个代理服务器,使用一个中间件 http-proxy-middleware,当我们在webpack中,配置proxy时,底层也是使用的 http-proxy-middleware
js
// 代理服务器 proxy.js
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
app.use(
"/cross",
createProxyMiddleware({
// 要代理的目标地址
target: "http://localhost:2023",
changeOrigin: true,
onProxyRes(proxyRes, req, res) {
// CORS解决跨域
res.header("Access-Control-Allow-Origin", req.headers.origin);
},
})
);
app.listen(2024, () => {
console.log("express代理服务器启动");
});
再修改一下客户端请求地址为代理服务器地址
js
btnEL.onclick = function () {
fetch("http://localhost:2024/cross");
};
然后请求成功
部署静态资源服务器
我们平时开发中使用webpack和vite等构建工具,用命令 npm run serve
,npm run dev
开启一个本地服务器,然后把我们在src
文件夹的源代码部署在本地服务器。在浏览器中打开项目,页面源和目标源都是代理服务器地址。
例如:
客户端代码,目标服务器代码都同上,将index.html放到public文件夹
修改一下代理服务器代码
js
// 代理服务器 proxy.js
const path = require("path");
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
//部署静态资源
app.use(express.static(path.resolve(__dirname, "./public")));
app.use(
"/cross",
createProxyMiddleware({
// 要代理的目标地址
target: "http://localhost:2023",
changeOrigin: true,
})
);
app.listen(2024, () => {
console.log("express代理服务器启动");
});
Nginx反向代理
什么是正向代理?Node代理就是正向代理,我们主动的
访问代理服务器地址,目标服务器不知道真实的客户端(隐藏真实客户端
)
反向代理就是客户端什么都不用配置,甚至都不知道自己的请求被代理了,被动的
代理,以为自己访问的就是真实的服务器(隐藏真实的服务器
)。
下载 nginx,运行后,在地址栏输入localhost,默认80端口, 可以看到启动成功的页面
打开下载的文件\conf\nginx.conf
,可以看到
conf
server {
#上图访问的就是这个80服务
listen 80;
server_name localhost;
# 上图显示的就是这个index.html文件
location / {
root html;
index index.html index.htm;
}
}
启动一个koa服务器
js
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const app = new Koa();
const router = new KoaRouter({ prefix: "/cross" });
router.get("/", (ctx) => {
ctx.body = {
msg: "node代理请求成功",
code: 200,
};
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa目标服务器启动"));
配置nginx
conf
server {
#上图访问的就是这个80服务
listen 80;
server_name localhost;
# 上图显示的就是这个index.html文件
location / {
root html;
index index.html index.htm;
}
# 代理的路径
location /cross {
proxy_pass http://localhost:2023;
}
}
客户端发送请求
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nginx反向代理</title>
</head>
<body>
<button id="btn">Nginx反向代理</button>
<script>
const btnEL = document.querySelector("#btn");
btnEL.onclick = function () {
fetch("http://localhost:80/cross");
};
</script>
</body>
</html>
没有设置CORS,出现跨域问题
修改一下nginx配置,设置CORS,同样,需要根据简单和非简单请求设置不同的响应头
conf
server {
#监听80端口
listen 80;
server_name localhost;
# 设置CORS
# 指定响应资源是否允许与给定的 origin 共享
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Credentials 'true';
# 配置允许跨域的请求方法
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,Options';
# 配置允许跨域的请求头
# add_header Access-Control-Allow-Headers 'Authorization,Content-Type,Accept,Origin,User-Agent,Cache-Control,X-Mx-ReqToken,X-Requested-With';
# 处理非简单请求 ,第一次预检 Preflight(method: OPTIONS)
if ($request_method = 'OPTIONS') {
return 200;
}
# 代理的路径
location /cross {
proxy_pass http://localhost:2023;
}
}
请求成功