bigywater.tech
Flask · 79

Flask-CORS 跨域处理

CORS 是服务端的访问控制吗?预检请求长什么样?为什么带 cookie 的请求特别容易失败?这篇既用 test_client 看 Flask-CORS 发出的头,也用真实的无头 Chrome 与两个不同端口的服务,看浏览器到底拦了什么。

请求照样被处理
不在白名单的跨域请求,服务端照常执行并返回 200,是浏览器不给 JS 看响应
预检
非简单请求先发 OPTIONS;预检不过,真正的 POST 根本不会发出
Allow-Credentials
带 cookie 的跨域请求需要 supports_credentials=True 并且 Allow-Origin 是具体的源

默认配置与 Origin

实测 · 本机执行输出
  带 Origin: 200 {'Access-Control-Allow-Origin': 'http://app.example', 'Vary': 'Origin'}
  不带 Origin: 200 {'Access-Control-Allow-Origin': '*'}

上一篇:Flask-SocketIO 长连接;下一篇:自定义 Flask Extension。

预检请求

实测 · 本机执行输出
   200 {'Access-Control-Allow-Origin': 'http://app.example', 'Access-Control-Allow-Headers': 'content-type, x-token', 'Access-Control-Allow-Methods': 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', 'Vary': 'Origin'} 正文长度 0
  methods=[GET,POST],请求 PUT 的预检: 200 {}

白名单与谁来拦

实测 · 本机执行输出
  Origin=http://app.example     → 200 {'Access-Control-Allow-Origin': 'http://app.example'} 正文={'ok': True}
  Origin=http://evil.example    → 200 {} 正文={'ok': True}
  evil 的预检: 200 {}

带凭证的请求

实测 · 本机执行输出
  origins="*" + credentials    → {'Access-Control-Allow-Origin': 'http://app.example', 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin'}
  origins=[A] + credentials    → {'Access-Control-Allow-Origin': 'http://app.example', 'Access-Control-Allow-Credentials': 'true'}

resources 与 cross_origin

实测 · 本机执行输出
  /api/x    Origin=http://app.example     → {'Access-Control-Allow-Origin': 'http://app.example', 'Access-Control-Expose-Headers': 'X-Total'}
  /private  Origin=http://app.example     → {}
  /one      Origin=http://one.example     → {'Access-Control-Allow-Origin': 'http://one.example'}
  /one      Origin=http://app.example     → {}

错误响应的 CORS 头

实测 · 本机执行输出
  /boom            → 500 {'Access-Control-Allow-Origin': 'http://app.example'}
  /nonexistent     → 404 {'Access-Control-Allow-Origin': 'http://app.example'}
  /missing-abort   → 403 {'Access-Control-Allow-Origin': 'http://app.example'}
  TESTING=False 下 /boom: 500 {'Access-Control-Allow-Origin': 'http://app.example'}

真实浏览器验证

实测 · 本机执行输出
  [接口没开 CORS]
    GET /data                        FAIL TypeError: Failed to fetch
    POST /data (JSON)                FAIL TypeError: Failed to fetch
    GET /data + X-Token 头            FAIL TypeError: Failed to fetch
    GET /me (credentials: include)   FAIL TypeError: Failed to fetch
    服务端实际收到的请求: ['GET /data origin=http://127.0.0.1:9602 cookie=None', 'OPTIONS /data origin=http://127.0.0.1:9602 cookie=None', 'OPTIONS /data origin=http://127.0.0.1:9602 cookie=None', 'GET /me origin=http://127.0.0.1:9602 cookie=abc123']
  [CORS(app) 默认配置]
    GET /data                        OK 200 {"method":"GET","ok":true}
    POST /data (JSON)                OK 200 {"method":"POST","ok":true}
    GET /data + X-Token 头            OK 200 {"method":"GET","ok":true}
    GET /me (credentials: include)   FAIL TypeError: Failed to fetch
    服务端实际收到的请求: ['GET /data origin=http://127.0.0.1:9604 cookie=None', 'OPTIONS /data origin=http://127.0.0.1:9604 cookie=None', 'POST /data origin=http://127.0.0.1:9604 cookie=None ctype=application/json', 'OPTIONS /data origin=http://127.0.0.1:9604 cookie=None', 'GET /data origin=http://127.0.0.1:9604 cookie=None', 'GET /me origin=http://127.0.0.1:9604 cookie=abc123']
  [CORS(app, origins=[页面源])]
    GET /data                        OK 200 {"method":"GET","ok":true}
    POST /data (JSON)                OK 200 {"method":"POST","ok":true}
    GET /data + X-Token 头            OK 200 {"method":"GET","ok":true}
    GET /me (credentials: include)   FAIL TypeError: Failed to fetch
    服务端实际收到的请求: ['GET /data origin=http://127.0.0.1:9606 cookie=None', 'OPTIONS /data origin=http://127.0.0.1:9606 cookie=None', 'POST /data origin=http://127.0.0.1:9606 cookie=None ctype=application/json', 'OPTIONS /data origin=http://127.0.0.1:9606 cookie=None', 'GET /data origin=http://127.0.0.1:9606 cookie=None', 'GET /me origin=http://127.0.0.1:9606 cookie=abc123']
  [origins=[页面源] + supports_credentials]
    GET /data                        OK 200 {"method":"GET","ok":true}
    POST /data (JSON)                OK 200 {"method":"POST","ok":true}
    GET /data + X-Token 头            OK 200 {"method":"GET","ok":true}
    GET /me (credentials: include)   OK 200 {"sid":"abc123"}
    服务端实际收到的请求: ['GET /data origin=http://127.0.0.1:9608 cookie=None', 'OPTIONS /data origin=http://127.0.0.1:9608 cookie=None', 'POST /data origin=http://127.0.0.1:9608 cookie=None ctype=application/json', 'OPTIONS /data origin=http://127.0.0.1:9608 cookie=None', 'GET /data origin=http://127.0.0.1:9608 cookie=None', 'GET /me origin=http://127.0.0.1:9608 cookie=abc123']

浏览器是本机的 Google Chrome 无头模式(--headless=new),页面与接口是两个不同端口的 gunicorn 服务。

分步演示

Flask-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,数字每次运行会有小幅抖动。Flask-CORS 6.0.5、本机 Google Chrome(无头模式)
    • 没有验证:Firefox 与 Safari 的差异、Private Network Access 相关的预检、allow_headers 精确白名单下的行为、vary_header 与缓存、Cookie 的 SameSite 属性对跨站带 cookie 请求的影响(本实验是同一主机不同端口,属于同站)、反向代理与 CORS 头重复的问题
    京ICP备2026067502号-1京公网安备11010502063487号