为 Next.js@14 中的 API Route 配置 CORS
本文的主要内容是简单介绍了 CORS 的相关内容及如何为 Next.js@14 中的 API Route 配置 CORS。
2023-11-29
CORS 跨源资源共享
Cross-Origin Resource Sharing 跨源资源共享, 指的是服务器允许托管在其他域或其他来源上的 Web 页面向其发送 HTTP 请求的一种机制。要理解 CORS,首先需要了解 Same-Origin Policy 同源策略。
Same-Origin Policy 同源策略
同源策略是一种 Web 安全策略,用于限制一个“源“的文档或者它加载的脚本与另一个“源“的资源进行交互。
所谓的“同源”指的是两个 URL 具有相同的主机(hostname),并且使用相同的协议和端口号。在 Web 应用中,跨源访问有以下这些限制:
列表 | 操作 | 是否可以跨源 |
---|---|---|
跨源网络访问 | 跨源写操作 | 一般是被允许的 |
跨源资源嵌入 | 一般是被允许的 | |
跨源读操作 | 一般是不被允许的 | |
跨源脚本 API 访问 | Window | window.blur 方法 |
window.close 方法 | ||
window.focus 方法 | ||
window.postMessage 方法 | ||
只读属性 window.closed | ||
只读属性 window.frames | ||
只读属性 window.length | ||
读/写属性 window.location | ||
只读属性 window.opener | ||
只读属性 window.parent | ||
只读属性 window.self | ||
只读属性 window.top | ||
只读属性 window.window | ||
Location | location.replace 方法 | |
只写属性 HTMLAnchorElement.href | ||
跨源数据存储访问 | Web Storage/IndexDB | 以源进行分割,无法跨源访问 |
CORS 的实现
CORS 需要浏览器和服务器相互配合才能实现。
CORS HTTP Headers
跨源资源共享标准新增了一组 HTTP Header,浏览器和服务器可以通过发送或响应这些 HTTP Header 实现跨域访问资源。
HTTP 头 | 添加方 | 说明 | 取值 | 备注 |
---|---|---|---|---|
Access-Control-Allow-Origin | 服务器 | 指示响应的资源是否可以被给定的来源共享 | origin 或 * | 如果 Access-Control-Allow-Credentials 为 true,则不能使用通配符 * |
Access-Control-Allow-Credentials | 服务器 | 指示当请求的凭证标记为 true 时,是否可以公开对该请求响应 | true 或 false | 当用在对 preflight 预检测请求的响应中时,它指定了实际的请求是否可以使用 credentials |
Access-Control-Allow-Headers | 服务器 | 用在对预检请求的响应中,指示实际的请求中可以使用哪些 HTTP 标头 | header-name[, header-name]* | |
Access-Control-Allow-Methods | 服务器 | 指定对预检请求的响应中,哪些 HTTP 方法允许访问请求的资源 | method[, method]* | |
Access-Control-Expose-Headers | 服务器 | 通过列出标头的名称,指示哪些标头可以作为响应的一部分公开 | header-name[, header-name]* | 不在列表中的 header 无法通过 JavaScript 获取 |
Access-Control-Max-Age | 服务器 | 指示预检请求的结果能被缓存多久 | delta-seconds | |
Access-Control-Request-Headers | 浏览器 | 用于发起一个预检请求,告知服务器正式请求会使用哪些 HTTP Headers | field-name[, field-name]* | |
Access-Control-Request-Method | 浏览器 | 用于发起一个预检请求,告知服务器正式请求会使用哪一种 HTTP Method | method | |
Origin | 浏览器 | 指示获取资源的请求是从什么源发起的 | hostname | Orign 中不包含 pathname;标头字段总是被发送。 |
Preflight request 预检请求
浏览器可以通过预检请求向服务器确认是否支持 CORS:
- 当浏览器发送非“简单请求”时,浏览器会自动发送一个预检请求(使用 OPTIONS 方法,并携带 和
Access-Control-Request-Method
头)向服务器确认是否可以发送该请求;Access-Control-Request-Headers
- 如果服务器允许该请求,则会响应该预检请求(携带对应的 头);
Access-Control-*
- 当浏览器收到该响应后,再发送正式的请求。
所谓的“简单请求”指的是:
- Request Method 为:GET、HEAD、POST;
- 手动添加的 Request Headers 只有以下几种:Accept、Accept-Language、Content-Language、Content-Type、Range;
- Content-Type Header 的 Value 为: text/plain 或 multipart/form-data 或 application/x-www-form-urlencoded;
- Range Header 的 Value 使用简单格式,即:bytes=256-、bytes=127-255;
- 使用 XMLHttpRequest 发送请求时,没有监听 XMLHttpRequest.upload 上的事件;
- 请求中没有使用 ReadableStream 对象。
在 Next.js@14 中为 API Route 实现 CORS
如上所述,CORS 是通过浏览器和服务器相互配合实现的。在现代浏览器中一般都实现了 CORS 规范,因此一般情况下只需要在服务器中进行响应的配置即可。
在 Next.js@14 中有以下几种方式可以为 API Route 配置 CORS:
- 在 中设置
next.config.js
选项:headers
const CORS_HEADERS = [
{
key: "Access-Control-Allow-Credentials",
value: "true"
},
{
key: "Access-Control-Allow-Origin",
value: "*"
},
{
key: "Access-Control-Allow-Methods",
value: "GET,DELETE,PATCH,POST,PUT"
},
{
key: "Access-Control-Allow-Headers",
value: "*"
},
];
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/api/:path*", // 为访问 /api/** 的请求添加 CORS HTTP Headers
headers: CORS_HEADERS
},
{
source: "/specific", // 为特定路径的请求添加 CORS HTTP Headers,
headers: CORS_HEADERS
}
]
}
}
module.exports = nextConfig
next.config.js
- 在 中配置:
middleware
import { NextResponse } from "next/server";
export function middleware() {
const resp = NextResponse.next()
resp.headers.append('Access-Control-Allow-Credentials', "true");
resp.headers.append('Access-Control-Allow-Origin', '*');
resp.headers.append('Access-Control-Allow-Methods', 'GET,DELETE,PATCH,POST,PUT');
resp.headers.append('Access-Control-Allow-Headers', '*');
return res
}
export const config = { matcher: '/api/:path*' };
middleware.ts
matcher
- 如果只是针对某些特定的 Route 才需要配置 CORS,除了通过 指定外,还可以单独在 API Route 中进行配置,比如:
matcher
import { NextResponse } from "next/server";
const appendCORS = (resp: NextResponse) => {
resp.headers.append("Access-Control-Allow-Credentials", "true");
resp.headers.append("Access-Control-Allow-Origin", "*");
resp.headers.append("Access-Control-Allow-Methods", "GET,DELETE,PATCH,POST,PUT");
resp.headers.append("Access-Control-Allow-Headers", "*");
return resp;
};
const setContentType = (resp: NextResponse, body: string) => {
resp.headers.set("Content-Type", "application/json; charset=UTF-8");
resp.headers.set("Content-Length", Buffer.from(body).byteLength + "");
return resp;
};
export function GET() {
const body = JSON.stringify({ status: 1 });
const resp = new NextResponse(body);
return appendCORS(setContentType(resp, body));
}
app/api/example/route.ts