这篇主要来聊一聊前端常见的跨域问题,以及后端如何处理 CORS 和预检请求 (Preflight)。
浏览器在什么情况下会发生跨域 浏览器通过“同源策略”限制不同源之间的资源交互,以保护用户隐私和安全。其中源 由三个部分组成:协议 、域名 和 端口 。只有这三者同时满足才是同源 。否则,就是跨域 ,向服务端发送请求时会触发浏览器的跨域限制,报以下错误:
text 复制
Access to fetch at 'https://server.suemor.com/api/posts' from origin 'https://suemor.com' CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
具体看下方 4 个例子。
跨域 以下是三种跨域情况:
不同协议
不同域名
不同端口
同源 下方这个是同源,没有跨域问题。
解决跨域 跨域问题通常在服务端解决,通过配置反向代理或修改后端代码。
跨域请求分为简单请求 和复杂请求 :
简单请求 对于同时满足以下三个条件的即为简单请求,服务器只需返回正确的 CORS 头,即 Access-Control-Allow-Origin :
请求方法 :GET、POST 或 HEAD。Content-Type :限于 application/x-www-form-urlencoded、multipart/form-data 或 text/plain。不包含自定义头 :如 Authorization。看以下 Express 示例:
typescript 复制
// 假设 web 端位于 http://localhost:3000
// 假设 server 端位于 http://localhost:5050
// web
fetch ( "http://localhost:5000/api/posts" , { method : "GET" } ) ;
// server
app . use ( ( req : Request , res : Response , next : NextFunction ) => {
res . setHeader ( "Access-Control-Allow-Origin" , "http://localhost:3000" ) ; // 或者 res.setHeader("Access-Control-Allow-Origin", "");
next ( ) ;
} ) ;
复杂请求 符合以下任意一条,即为复杂请求:
使用 PUT、DELETE 、PATCH 方法。 Content-Type: application/json。 包含自定义请求头(如 Authorization)。 复杂请求比较特殊,浏览器会先发送一个 OPTIONS 方法的预检请求 (Preflight),检查服务器是否允许该跨域请求。如果不允许,则直接抛出 CORS 错误,不再发送实际请求。
CORS 错误
因此,我们需要单独处理这个预检请求 (Preflight):
typescript 复制
// 假设 web 端位于 http://localhost:3000
// 假设 server 端位于 http://localhost:5050
// web
fetch ( "http://localhost:5050/api/data" , {
method : "PUT" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : "Bearer token" ,
} ,
body : JSON . stringify ( { data : "example" } ) ,
} ) ;
// server
app . use ( ( req : Request , res : Response , next : NextFunction ) => {
res . setHeader ( "Access-Control-Allow-Origin" , "http://localhost:3000" ) ;
if ( req . method === "OPTIONS" ) {
res . setHeader ( "Access-Control-Allow-Origin" , "http://localhost:3000" ) ;
res . header ( "Access-Control-Allow-Methods" , "PUT" ) ;
res . header ( "Access-Control-Allow-Headers" , "Content-Type, Authorization" ) ;
res . status ( 200 ) . end ( ) ;
return ;
}
next ( ) ;
} ) ;
设置 Access-Control-Max-Age 复杂请求在无缓存或缓存失效时会发送两次请求:Preflight(OPTIONS)和实际请求,这会增加网络开销。为此,服务器可以通过设置 Access-Control-Max-Age 响应头来控制浏览器缓存预检结果的时长。这个头字段的值表示缓存的有效期(以秒为单位)。在缓存有效期内,浏览器会复用之前的预检结果,跳过对相同接口的 Preflight 请求,从而提升性能。
typescript 复制
res . header ( 'Access-Control-Max-Age' , '86400' ) ; // 缓存 1 天
支持跨域 Cookie 的配置 在跨域场景下,如果后端响应头返回 Set-Cookie,默认不会生效,因为设置 Cookie 需要额外配置以绕过浏览器的安全限制。核心是启用 Access-Control-Allow-Credentials 并明确指定 Access-Control-Allow-Origin。以下是一个 Express 示例:
typescript 复制
app . use ( ( req : Request , res : Response , next : NextFunction ) => {
res . setHeader ( "Access-Control-Allow-Origin" , "http://localhost:3000" ) ; // 一定要指定具体地址,不能为 *
res . setHeader ( "Access-Control-Allow-Credentials" , "true" ) ; //添加这个
if ( req . method === "OPTIONS" ) {
res . setHeader ( "Access-Control-Allow-Origin" , "http://localhost:3000" ) ; // 一定要指定具体地址,不能为 *
res . setHeader ( "Access-Control-Allow-Methods" , "GET, POST, PUT, DELETE" ) ;
res . setHeader ( "Access-Control-Allow-Headers" , "Content-Type, Authorization" ) ;
res . setHeader ( "Access-Control-Max-Age" , "86400" ) ;
res . status ( 200 ) . end ( ) ;
return ;
}
next ( ) ;
} ) ;
app . post ( "/api/posts" , ( req : Request , res : Response ) => {
res . cookie ( "sessionId" , "123456789" , {
httpOnly : true ,
secure : process . env . NODE_ENV === "production" ,
sameSite : "none" ,
maxAge : 24 * 60 * 60 * 1000 ,
} ) ;
res . json ( { success : true , message : "Cookie set" } ) ;
} ) ;
注意这里 Access-Control-Allow-Origin 一定要指定具体的地址,不能设置为 Access-Control-Allow-Origin: *,否则 Cookie 无效。
前端如果使用 fetch 调用,则一定要加上 credentials: "include"否则无法设置 Cookie。如果是 axios 则加上 withCredentials: true。
typescript 复制
//fetch
fetch ( "http://localhost:5050/api/posts" , {
method : "POST" ,
credentials : "include" , // 允许携带和接收 Cookie
} ) . then ( ( res ) => res . json ( ) ) ;
//axios
axios ( {
url : "http://localhost:5050/api/posts" ,
method : "POST" ,
withCredentials : true , // 允许携带和接收 Cookie
} )
. then ( ( res ) => res . data )
. catch ( ( err ) => console . error ( err ) ) ;