修改 Flask 的默认响应头实现跨域(CORS)支持

  • 本站文章除注明转载外,均为本站原创或者翻译。
  • 本站文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商;
  • 本站部分原创和翻译文章提供markdown格式源码,欢迎使用文章源码进行转载;
  • 本博客采用 WPCMD 维护;
  • 本文标题:修改 Flask 的默认响应头实现跨域(CORS)支持
  • 本文链接:http://zengrong.net/post/2615.htm

要提供一个 RESTful API ,就必须考虑 跨域请求(CORS) 问题。在 Flask 中,我们可以进行这样的简单处理:

@main.route('/', methods=['GET'])
def index():
    resp = jsonify({'error':False})
    # 跨域设置
    resp.headers['Access-Control-Allow-Origin'] = '*'
    return resp

当路由较多的时候,这样写未免不优雅,我们可以封装一个方法 responseto 用来代替 jsonify:

def responseto(message=None, error=None, data=None, **kwargs):
    """ 封装 json 响应
    """
    # 如果提供了 data,那么不理任何其他参数,直接响应 data
    if not data:
        data = kwargs
        data['error'] = error
        if message:
            # 除非显示提供 error 的值,否则默认为 True
            # 意思是提供了 message 就代表有 error
            data['message'] = message
            if error is None:
                data['error'] = True
        else:
            # 除非显示提供 error 的值,否则默认为 False
            # 意思是没有提供 message 就代表没有 error
            if error is None:
                data['error'] = False
    if not isinstance(data, dict):
        data = {'error':True, 'message':'data 必须是一个 dict!'}
    resp = jsonify(data)
    # 跨域设置
    resp.headers['Access-Control-Allow-Origin'] = '*'
    return resp

这样,上面的代码可以简化为:

@main.route('/', methods=['GET'])
def index():
    return responseto()

要响应一个错误消息,可以简化为:

responseto('发生了一个错误!')

但是,当我使用 PUT 方法请求时,出现了这样的错误:

XMLHttpRequest cannot load http://127.0.0.1:5000/account/status/. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:5001' is therefore not allowed access.

从当时请求的内容(见下方)可以看出,请求变成了 OPTIONS 而不是 PUT 。这是由于根据 CORS 规范 (via) ,浏览器会做一次 preflight 请求,这次请求询问服务器支持哪些方法。

OPTIONS /account/status/ HTTP/1.1
Host: 127.0.0.1:5000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: PUT
Origin: http://localhost:5001
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Mobile Safari/537.36
Access-Control-Request-Headers:
Accept: */*
Referer: http://localhost:5001/account/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

因此,上面提到的使用 responseto 的方法就没有作用了。因为任何一个路由都有可能包含 preflight 请求。

我们可以通过继承 Flask 的 Response 类来实现这个需求,让 flask 回复的任何响应都带有 Access-Control-Allow-* 的 HEAD。通过设置 Flask app 的 response_class 属性可以让 Flask 使用我们自定义的子类作为响应。

from flask import Flask, Response

class MyResponse(Response):
    pass

def create_app():
    app = Flask(__name__)
    app.response_class = MyResponse

我们需要在 MyResponse 类中对 headers 进行一些操作。为此我们需要了解 Flask Response 的源码实现。

Flask 的 Response 是对 werkzeug.wrappers.Response 的一个简单继承。下面是 flask 中 Response 的源码实现(位于 wrappers.py 中):

from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase

class Response(ResponseBase):
    """The response object that is used by default in Flask.  Works like the
    response object from Werkzeug but is set to have an HTML mimetype by
    default.  Quite often you don't have to create this object yourself because
    :meth:`~flask.Flask.make_response` will take care of that for you.

    If you want to replace the response object used you can subclass this and
    set :attr:`~flask.Flask.response_class` to your subclass.
    """
    default_mimetype = 'text/html'

没错,就是仅此而已。因此我们还需要去看 werkzeug.wrappers.Response 的源码。下面是一点点节选:

    def __init__(self, response=None, status=None, headers=None,
                 mimetype=None, content_type=None, direct_passthrough=False):
        if isinstance(headers, Headers):
            self.headers = headers
        elif not headers:
            self.headers = Headers()
        else:
            self.headers = Headers(headers)

由于参数太多,在实现继承的时候,我们仅保留一个 response 参数,其余的使用 **kwargs 代替:

from werkzeug.datastructures import Headers

class MyResponse(Response):
    def __init__(self, response=None, **kwargs):
        kwargs['headers'] = ''
        headers = kwargs.get('headers')
        # 跨域控制 
        origin = ('Access-Control-Allow-Origin', '*')
        methods = ('Access-Control-Allow-Methods', 'HEAD, OPTIONS, GET, POST, DELETE, PUT')
        if headers:
            headers.add(*origin)
            headers.add(*methods)
        else:
            headers = Headers([origin, methods])
        kwargs['headers'] = headers
        return super().__init__(response, **kwargs)

使用上面的代码可以方便地实现跨域支持,根据需要调整 methods 和 origin 的值即可。

关于自定义响应类,Miguel 的这篇文章写得更详细: Customizing the Flask Response Class

(全文完)