bigywater.tech
Flask · 112

CORS 预检请求

什么样的请求会触发预检?预检结果能缓存多久?为什么加了 Authorization 头就失败?为什么认证中间件会让预检失败?这篇用真实的 Chrome 与服务端请求日志,把预检的每个细节对照出来。

6 → 3
同样八个请求,Max-Age: 0 与 Max-Age: 60 时服务端收到的 OPTIONS 个数
Authorization
必须在 Access-Control-Allow-Headers 里明确列出
401
预检返回 401 时所有非简单请求都失败

哪些请求触发预检

实测 · 本机执行输出
    GET(简单请求)                        OK 200
    POST text/plain(简单请求)            OK 200
    POST application/json            OK 200
    POST application/json(再来一次)      OK 200
    GET + 自定义头 X-Token               OK 200
    GET + Authorization 头            FAIL Failed to fetch
    PUT                              OK 200
    DELETE                           OK 200
    服务端收到的请求顺序: ['GET /data', 'POST /data [text/plain]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'POST /data [application/json]', 'POST /data [application/json]', 'GET /data', 'OPTIONS /data [预检:方法=GET 头=authorization]', 'OPTIONS /data', 'PUT /data', 'DELETE /data']
    其中 OPTIONS 个数: 3

上一篇:API 限流;下一篇:ETag API 缓存。

第 79 篇用 Flask-CORS 验证过 CORS 的基本行为,这一篇专门看预检。

Max-Age 缓存

实测 · 本机执行输出
    GET(简单请求)                        OK 200
    POST text/plain(简单请求)            OK 200
    POST application/json            OK 200
    POST application/json(再来一次)      OK 200
    GET + 自定义头 X-Token               OK 200
    GET + Authorization 头            FAIL Failed to fetch
    PUT                              OK 200
    DELETE                           OK 200
    服务端收到的请求顺序: ['GET /data', 'POST /data [text/plain]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'POST /data [application/json]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'POST /data [application/json]', 'OPTIONS /data [预检:方法=GET 头=x-token]', 'GET /data', 'OPTIONS /data [预检:方法=GET 头=authorization]', 'OPTIONS /data', 'PUT /data', 'OPTIONS /data', 'DELETE /data']
    其中 OPTIONS 个数: 6

Allow-Headers

实测 · 本机执行输出
    GET(简单请求)                        OK 200
    POST text/plain(简单请求)            OK 200
    POST application/json            FAIL Failed to fetch
    POST application/json(再来一次)      FAIL Failed to fetch
    GET + 自定义头 X-Token               FAIL Failed to fetch
    GET + Authorization 头            FAIL Failed to fetch
    PUT                              OK 200
    DELETE                           OK 200
    服务端收到的请求顺序: ['GET /data', 'POST /data [text/plain]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'OPTIONS /data [预检:方法=GET 头=x-token]', 'OPTIONS /data [预检:方法=GET 头=authorization]', 'OPTIONS /data', 'PUT /data', 'DELETE /data']
    其中 OPTIONS 个数: 5

OPTIONS 被认证拦截

实测 · 本机执行输出
    GET(简单请求)                        OK 200
    POST text/plain(简单请求)            OK 200
    POST application/json            FAIL Failed to fetch
    POST application/json(再来一次)      FAIL Failed to fetch
    GET + 自定义头 X-Token               FAIL Failed to fetch
    GET + Authorization 头            FAIL Failed to fetch
    PUT                              FAIL Failed to fetch
    DELETE                           FAIL Failed to fetch
    服务端收到的请求顺序: ['GET /data', 'POST /data [text/plain]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'OPTIONS /data [预检:方法=POST 头=content-type]', 'OPTIONS /data [预检:方法=GET 头=x-token]', 'OPTIONS /data [预检:方法=GET 头=authorization]', 'OPTIONS /data', 'OPTIONS /data']
    其中 OPTIONS 个数: 6

浏览器是本机的 Google Chrome 无头模式,页面与接口是两个不同端口的 gunicorn 服务,CORS 头是手写的(没有用 Flask-CORS)。

分步演示

CORS 预检请求
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。本机 Google Chrome(无头模式)
    • 没有验证:Firefox 与 Safari 的预检缓存上限与行为差异(Chrome 对 Max-Age 有自己的上限)、Access-Control-Allow-Headers: * 的通配行为、预检遇到重定向的处理、Private Network Access 的预检、带凭证请求的预检
    京ICP备2026067502号-1京公网安备11010502063487号