一、为什么Flask会遇到CORS问题

现代Web开发中,前后端分离已经成为主流架构模式。前端可能运行在http://localhost:3000,而后端API服务运行在http://localhost:5000,浏览器出于安全考虑,会阻止这种跨域请求。这就是所谓的同源策略限制。

Flask作为轻量级Python Web框架,默认没有内置CORS支持。当你的前端应用尝试访问Flask API时,浏览器会抛出类似这样的错误:

Access to XMLHttpRequest at 'http://localhost:5000/api' from origin 'http://localhost:3000' has been blocked by CORS policy

二、解决Flask CORS问题的三种主流方案

2.1 使用Flask-CORS扩展(推荐方案)

这是最简单直接的解决方案。Flask-CORS是专门为Flask设计的扩展,只需几行代码就能搞定。

from flask import Flask
from flask_cors import CORS  # 导入CORS模块

app = Flask(__name__)
CORS(app)  # 启用CORS,允许所有域名访问所有路由

@app.route("/api")
def hello():
    return {"message": "Hello, CORS!"}

if __name__ == "__main__":
    app.run()

如果想进行更精细的控制,可以这样配置:

CORS(app, resources={
    r"/api/*": {
        "origins": ["http://localhost:3000", "https://your-production-site.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"]
    }
})

2.2 手动添加CORS头信息

如果你不想使用第三方扩展,也可以手动添加响应头:

from flask import Flask, jsonify, make_response

app = Flask(__name__)

@app.route("/api")
def hello():
    response = make_response(jsonify({"message": "Hello, CORS!"}))
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
    return response

@app.after_request
def after_request(response):
    # 这个钩子函数会在每个请求后执行
    response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
    response.headers.add("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
    return response

if __name__ == "__main__":
    app.run()

2.3 使用Nginx反向代理

对于生产环境,更推荐使用Nginx作为反向代理来解决CORS问题:

server {
    listen 80;
    server_name api.yourdomain.com;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        proxy_pass http://localhost:5000;
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
    }
}

三、处理预检请求(OPTIONS)的特殊情况

对于复杂请求(如带自定义头或非简单方法的请求),浏览器会先发送OPTIONS预检请求。我们需要特别处理:

@app.route("/api", methods=["GET", "POST", "OPTIONS"])
def handle_api():
    if request.method == "OPTIONS":
        # 处理预检请求
        response = make_response()
        response.headers["Access-Control-Allow-Origin"] = "http://localhost:3000"
        response.headers["Access-Control-Allow-Methods"] = "GET, POST"
        response.headers["Access-Control-Allow-Headers"] = "Content-Type, X-Custom-Header"
        response.headers["Access-Control-Max-Age"] = "86400"  # 24小时缓存
        return response
    
    # 处理实际请求
    return jsonify({"data": "Your actual response here"})

四、实际开发中的注意事项

  1. 不要在生产环境使用通配符(*):虽然Access-Control-Allow-Origin: *很方便,但会带来安全隐患。应该明确指定允许的域名。

  2. 注意凭据(cookie)的处理:如果需要发送cookie,必须满足三个条件:

    CORS(app, supports_credentials=True)  # Flask-CORS配置
    

    前端请求需要设置:

    fetch(url, {
      credentials: 'include'
    });
    

    并且不能使用通配符,必须指定具体域名:

    response.headers["Access-Control-Allow-Origin"] = "http://your-frontend.com"
    
  3. 缓存预检请求:通过设置Access-Control-Max-Age头可以减少OPTIONS请求的次数。

  4. 处理复杂请求头:如果你的API需要接收自定义头,必须在Access-Control-Allow-Headers中明确列出。

  5. 考虑使用环境变量:不同环境(开发/测试/生产)可能需要不同的CORS配置:

    allowed_origins = ["http://localhost:3000"] if os.environ.get("FLASK_ENV") == "development" else ["https://production-site.com"]
    CORS(app, origins=allowed_origins)
    

五、不同场景下的最佳实践

  1. 开发环境:为了方便,可以使用通配符或Flask-CORS的默认配置。

  2. 测试环境:应该模拟生产环境的配置,指定具体的测试域名。

  3. 生产环境

    • 使用Nginx处理CORS
    • 严格限制允许的域名
    • 启用HTTPS
    • 考虑使用CSRF令牌等额外安全措施
  4. 微服务架构:如果有多服务需要共享CORS配置,可以考虑将配置集中管理,或使用API网关统一处理。

六、常见问题排查指南

  1. 检查浏览器控制台错误:CORS错误通常会显示详细的错误信息,包括被拒绝的请求方法和缺少的头部。

  2. 使用CURL测试

    curl -H "Origin: http://your-frontend.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" -X OPTIONS --verbose http://your-api.com/endpoint
    
  3. 检查响应头:确保所有必要的CORS头都存在且值正确。

  4. 顺序问题:确保在返回响应前添加了CORS头,特别是在使用错误处理时。

  5. 检查Flask路由:确保OPTIONS方法已添加到路由的methods参数中。

七、总结

跨域问题是现代Web开发中无法回避的挑战,但通过合理使用Flask-CORS扩展、手动设置响应头或配置反向代理,我们可以优雅地解决这个问题。关键是根据不同环境选择合适的方案,并始终牢记安全最佳实践。记住,CORS虽然是个小配置,但它关系到整个应用的安全性和可用性,值得投入时间把它做好。