FumadocsZDecode
网络 & HTTP

跨域(CORS)

浏览器的同源策略限制了不同源之间的资源访问。CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器和服务器协商的标准机制,允许服务器声明哪些源可以访问其资源。

同源策略

两个 URL 同源要求协议、域名、端口三者完全一致。

URLhttps://example.com/page 同源?原因
https://example.com/other同协议、同域名、同端口
http://example.com/page协议不同(http vs https)
https://api.example.com/page子域名不同
https://example.com:8080/page端口不同
https://other.com/page域名不同

同源策略限制的是读取跨域响应,而不是发出请求。跨域请求可以发出,服务端会处理,但浏览器会拦截响应,不让页面脚本读取。

请求分类

CORS 将跨域请求分为两类,处理流程不同。

简单请求

同时满足以下条件的请求直接发送,无需预检:

  • 方法为 GETPOSTHEAD
  • 请求头仅包含 AcceptAccept-LanguageContent-LanguageContent-Type
  • Content-Type 限于 text/plainmultipart/form-dataapplication/x-www-form-urlencoded

浏览器在请求头中携带 Origin,服务端响应中包含 Access-Control-Allow-Origin 时浏览器才放行:

请求:
Origin: https://app.example.com

响应:
Access-Control-Allow-Origin: https://app.example.com

预检请求(Preflight)

不满足简单请求条件的请求(如 PUTDELETE、携带自定义头、Content-Type: application/json),浏览器先发一个 OPTIONS 请求询问服务端是否允许,通过后再发实际请求。

第一步:浏览器发 OPTIONS 预检
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

第二步:服务端响应允许
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

第三步:浏览器发实际请求
PUT /api/data HTTP/1.1
Origin: https://app.example.com
Content-Type: application/json

Access-Control-Max-Age 指定预检结果的缓存时间(秒),避免每次请求都触发预检。

CORS 响应头

响应头作用示例值
Access-Control-Allow-Origin允许的源,* 表示所有源https://app.example.com
Access-Control-Allow-Methods允许的 HTTP 方法GET, POST, PUT, DELETE
Access-Control-Allow-Headers允许的请求头Content-Type, Authorization
Access-Control-Allow-Credentials是否允许携带 Cookietrue
Access-Control-Expose-Headers允许前端读取的响应头X-Custom-Header
Access-Control-Max-Age预检结果缓存时间(秒)86400

Access-Control-Allow-Origin 设为 * 时,不能同时使用 Access-Control-Allow-Credentials: true,浏览器会拒绝。

默认情况下跨域请求不携带 Cookie。需要同时在前端和服务端开启:

// 前端:请求时设置 withCredentials
fetch('https://api.example.com/data', {
  credentials: 'include',
})

// axios
axios.get('https://api.example.com/data', {
  withCredentials: true,
})
# 服务端响应头(必须指定具体源,不能用 *)
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

服务端配置

Nginx

location /api/ {
  add_header Access-Control-Allow-Origin $http_origin always;
  add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
  add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
  add_header Access-Control-Allow-Credentials true always;

  if ($request_method = OPTIONS) {
    add_header Access-Control-Max-Age 86400;
    return 204;
  }

  proxy_pass http://backend;
}

Node.js(Express)

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.setHeader('Access-Control-Allow-Credentials', 'true')

  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Max-Age', '86400')
    return res.sendStatus(204)
  }
  next()
})

或使用 cors 中间件:

import cors from 'cors'

app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400,
}))

开发环境代理

开发时前端和后端通常端口不同,产生跨域。最简单的方案是在开发服务器配置代理,请求由服务端转发,浏览器看到的是同源请求。

Vite

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
}

webpack-dev-server

// webpack.config.js
devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:3000',
      changeOrigin: true,
    },
  },
},

代理只在开发环境有效,生产环境仍需服务端正确配置 CORS 响应头,或将前后端部署到同一域名下。

On this page