目录
- 1,介绍
- 2,简单请求
- 判定
- 交互规范
- 3,非简单请求
- 交互规范
- 1,发送预检请求
- 2,预检请求响应
- 3,浏览器发送真实的请求,服务器完成真实的响应。
- 附带身份凭证
相关内容:
浏览器同源策略和跨域问题
跨域-JSONP
1,介绍
CORS 是基于 http1.1 的一种跨域解决方案,它的全称是 Cross-Origin Resource Sharing(跨域资源共享)。MDN参考
总体思路:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许。
而一个请求可以附带很多信息,从而对服务器有不同的影响。有的只是获取数据,有的会修改数据。
CORS 针对不同的请求,规定了3种交互模式:
- 简单请求
- 需要预检的请求
- 附带身份凭证的请求
这3种模式层层递进,请求可以做的事越来越多,要求也越来越严格。
当浏览器发起请求(XMLHttpRequest 还是 fetch api)之前,都会先判定下属于哪一类请求模式
2,简单请求
判定
当请求同时满足以下条件时,浏览器会认为它是一个简单请求:
请求方法 | get、post、head |
请求头仅允许包含安全的字段 | Accept、Accept-Language、Content-Language、Content-Type、Range |
Content-Type 仅允许 | text/plain、multipart/form-data、application/x-www-form-urlencoded |
head 请求,比如当浏览器下载文件时,可能不知道文件有多大,服务器如果立即响应读取文件会占用服务器资源。
但浏览器可能又不想下载了,所以可以先发送一个 head 请求,来让服务器告知这个文件的大小(通过响应头)。
当真正想下载时,再发送下载请求。
举例:
// 简单请求(默认 get 请求)
fetch("http://example.com/api/list");
// 简单请求
fetch("http://example.com/api/list", {
method: "POST"
})
// 非简单请求(请求方法不满足)
fetch("http://example.com/api/list", {
method: "PUT"
})
// 非简单请求(有额外的请求头)
fetch("http://example.com/api/list", {
headers: {
a: 1
}
})
// 非简单请求(请求头 content-type 不满足要求)
fetch("http://example.com/api/list", {
method: "POST",
headers: {
"content-type": "application/json"
}
})
交互规范
1,请求头中会自动加入 Origin
字段
2,服务器响应头中应包含 Access-Control-Allow-Origin
字段。
当服务器收到请求后,如果允许请求跨域访问,则需要在请求头中添加该字段。值可以是:
*
,表示无限制。- 具体的源,比如
http://localhost:5500
交互过程如下图示:
举例:在 http://localhost:5500/index.html
页面发送请求 fetch("http://localhost:3001/api/login", { method: "POST" })
:
3,非简单请求
这样的请求一般会对服务器有特殊的要求,常见的比如 PUT
或 DELETE
请求,或者 Content-Type: application/json
。
交互规范
- 浏览器发送预检请求,询问服务器是否允许
- 预检请求响应,服务器允许
- 浏览器发送真实请求,服务器完成真实的响应
比如对这个请求:Content-Type: application/json
+ 有自定义的请求头:
fetch("http://localhost:3001/api/login", {
method: "POST"
headers: {
"Content-Type": "application/json",
"a": "aa",
"b": "bb",
},
body: JSON.stringify({
name: "下雪天的夏风",
pwd: "123",
}),
})
1,发送预检请求
特点:
- 请求方法是
OPTIONS
- 没有请求体
- 请求头中包含
Origin
:请求的源,和简单请求的一样。Access-Control-Request-Method
:后续真实请求将使用的请求方法。Access-Control-Request-Headers
:后续真实请求会额外发送的头信息字段。
2,预检请求响应
服务器收到预检请求后,如果允许这样的请求,需要在响应头中添加如下的字段(不需要响应任何请求体):
Access-Control-Allow-Origin
:允许的源,和简单请求的一样。Access-Control-Allow-Methods
:表示允许的后续真实的请求方法。Access-Control-Allow-Headers
:表示允许改动的请求头。Access-Control-Max-Age
:告诉浏览器在多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了,非必填。
预检请求的判断可以放到中间件中,也可以使用已有的中间件 koa-cors
3,浏览器发送真实的请求,服务器完成真实的响应。
一旦通过预检请求,后续过程和简单请求一致。
整个过程:
代码实现:
客户端:
<button id="btn">发送请求</button>
<script>
const btn = document.getElementById("btn");
btn.addEventListener("click", async function () {
const response = await fetch("http://localhost:3001/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"a": "aa",
"b": "bb",
},
body: JSON.stringify({
name: "下雪天的夏风",
pwd: "123",
})
});
console.log(response.headers.get("Content-Type")); // 获取指定响应头
response.json().then((res) => console.log(res));
});
</script>
服务端:
const Koa = require("koa");
const Router = require("koa-router");
const { bodyParser } = require("@koa/bodyparser");
const app = new Koa();
const router = new Router();
router.post("/api/login", (ctx) => {
ctx.set("Access-Control-Allow-Origin", "http://localhost:5500");
const { name, pwd } = ctx.request.body;
if (name === "下雪天的夏风" && pwd === "123") {
ctx.set("set-cookie", `token=aaa; domain=localhost; max-age=1000`);
ctx.body = {
code: 200,
msg: "登录成功",
};
} else {
ctx.body = {
code: 500,
msg: "用户名或密码错误",
};
}
});
router.options("/api/login", (ctx) => {
ctx.set("Access-Control-Allow-Origin", "http://localhost:5500");
ctx.set("Access-Control-Allow-Methods", "post");
ctx.set("Access-Control-Allow-Headers", "content-type,a,b");
ctx.set("Access-Control-Max-Age", "10");
ctx.body = "success";
});
app.use(bodyParser()).use(router.routes());
app.listen(3001);
预检请求:
真实的请求:
附带身份凭证
注意到,真实请求会设置 cookie,但并没有设置成功:
这是因为默认情况下,跨域请求并不会设置 cookie,同时请求也不会附带 cookie。
而想要实现附带身份的请求(也就是附带 cookie),需要2点
- 浏览器配置附带 cookie
// xhr
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// fetch api
fetch(url, {
credentials: "include" // 默认是 same-origin,同源才会附带。
})
- 服务器响应头中添加字段
Access-Control-Allow-Credentials: true
,来明确告知客户端允许这样的凭据。
// koa
ctx.set("Access-Control-Allow-Credentials", "true");
注意:
- 如果是非简单请求,在预检请求和真实请求中都需要添加该响应头!否则还是会报跨域错误。
- 对于附带身份的请求,服务器不得设置
Access-Control-Allow-Origin: *
。
MDN参考-打开页面往上滑一点
以上。
阮一峰-CORS参考