CORS 预检请求

理解 HTTP 跨域时的 Options 预检请求机制

问题

HTTP 跨域请求时,浏览器为什么会发送 Options 预检请求?什么情况下会触发?如何处理?

解答

什么是预检请求

预检请求(Preflight Request)是浏览器在发送”非简单请求”前,自动发送的一个 OPTIONS 请求,用于询问服务器是否允许该跨域请求。

简单请求 vs 非简单请求

简单请求需同时满足:

  1. 方法为 GET、HEAD、POST 之一
  2. 只包含安全的请求头:Accept、Accept-Language、Content-Language、Content-Type
  3. Content-Type 仅限:text/plain、multipart/form-data、application/x-www-form-urlencoded

非简单请求(会触发预检):

// 触发预检:使用了 PUT 方法
fetch('https://api.example.com/data', {
  method: 'PUT',
  body: JSON.stringify({ name: 'test' })
});

// 触发预检:自定义请求头
fetch('https://api.example.com/data', {
  headers: {
    'X-Custom-Header': 'value'
  }
});

// 触发预检:Content-Type 为 application/json
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'test' })
});

预检请求流程

浏览器                                服务器
  |                                    |
  |  1. OPTIONS /api/data              |
  |  Origin: https://example.com       |
  |  Access-Control-Request-Method: PUT|
  |  Access-Control-Request-Headers: X-Custom-Header
  | ---------------------------------> |
  |                                    |
  |  2. 200 OK                         |
  |  Access-Control-Allow-Origin: *    |
  |  Access-Control-Allow-Methods: PUT |
  |  Access-Control-Allow-Headers: X-Custom-Header
  |  Access-Control-Max-Age: 86400     |
  | <--------------------------------- |
  |                                    |
  |  3. PUT /api/data (实际请求)        |
  | ---------------------------------> |

服务器端配置

Node.js + Express:

const express = require('express');
const app = express();

// CORS 中间件
app.use((req, res, next) => {
  // 允许的源
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  // 允许的方法
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  // 允许的请求头
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header');
  // 预检请求缓存时间(秒)
  res.header('Access-Control-Max-Age', '86400');
  // 允许携带凭证
  res.header('Access-Control-Allow-Credentials', 'true');

  // 直接响应预检请求
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

app.put('/api/data', (req, res) => {
  res.json({ success: true });
});

app.listen(3000);

Nginx 配置:

location /api/ {
    # 预检请求处理
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' '86400';
        return 204;
    }

    add_header 'Access-Control-Allow-Origin' '$http_origin';
    add_header 'Access-Control-Allow-Credentials' 'true';
    
    proxy_pass http://backend;
}

预检请求的响应头

响应头说明
Access-Control-Allow-Origin允许访问的源
Access-Control-Allow-Methods允许的 HTTP 方法
Access-Control-Allow-Headers允许的请求头
Access-Control-Max-Age预检结果缓存时间
Access-Control-Allow-Credentials是否允许携带 Cookie

关键点

  • 预检请求是浏览器自动发送的 OPTIONS 请求,用于检查服务器是否允许跨域
  • 简单请求(GET/HEAD/POST + 安全头 + 限定 Content-Type)不触发预检
  • 使用自定义头、PUT/DELETE 方法、application/json 等会触发预检
  • Access-Control-Max-Age 可缓存预检结果,减少 OPTIONS 请求次数
  • 服务器需正确响应 OPTIONS 请求并返回相应的 CORS 头