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

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

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

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

 1def responseto(message=None, error=None, data=None, **kwargs):
 2    """ 封装 json 响应
 3    """
 4    # 如果提供了 data,那么不理任何其他参数,直接响应 data
 5    if not data:
 6        data = kwargs
 7        data['error'] = error
 8        if message:
 9            # 除非显示提供 error 的值,否则默认为 True
10            # 意思是提供了 message 就代表有 error
11            data['message'] = message
12            if error is None:
13                data['error'] = True
14        else:
15            # 除非显示提供 error 的值,否则默认为 False
16            # 意思是没有提供 message 就代表没有 error
17            if error is None:
18                data['error'] = False
19    if not isinstance(data, dict):
20        data = {'error':True, 'message':'data 必须是一个 dict!'}
21    resp = jsonify(data)
22    # 跨域设置
23    resp.headers['Access-Control-Allow-Origin'] = '*'
24    return resp

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

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

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

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

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

1XMLHttpRequest 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 请求,这次请求询问服务器支持哪些方法。

 1OPTIONS /account/status/ HTTP/1.1
 2Host: 127.0.0.1:5000
 3Connection: keep-alive
 4Pragma: no-cache
 5Cache-Control: no-cache
 6Access-Control-Request-Method: PUT
 7Origin: http://localhost:5001
 8User-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
 9Access-Control-Request-Headers:
10Accept: */*
11Referer: http://localhost:5001/account/
12Accept-Encoding: gzip, deflate, sdch, br
13Accept-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 使用我们自定义的子类作为响应。

1from flask import Flask, Response
2
3class MyResponse(Response):
4    pass
5
6def create_app():
7    app = Flask(__name__)
8    app.response_class = MyResponse

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

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

 1from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
 2
 3class Response(ResponseBase):
 4    """The response object that is used by default in Flask.  Works like the
 5    response object from Werkzeug but is set to have an HTML mimetype by
 6    default.  Quite often you don't have to create this object yourself because
 7    :meth:`~flask.Flask.make_response` will take care of that for you.
 8
 9    If you want to replace the response object used you can subclass this and
10    set :attr:`~flask.Flask.response_class` to your subclass.
11    """
12    default_mimetype = 'text/html'

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

1    def __init__(self, response=None, status=None, headers=None,
2                 mimetype=None, content_type=None, direct_passthrough=False):
3        if isinstance(headers, Headers):
4            self.headers = headers
5        elif not headers:
6            self.headers = Headers()
7        else:
8            self.headers = Headers(headers)

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

 1from werkzeug.datastructures import Headers
 2
 3class MyResponse(Response):
 4    def __init__(self, response=None, **kwargs):
 5        kwargs['headers'] = ''
 6        headers = kwargs.get('headers')
 7        # 跨域控制 
 8        origin = ('Access-Control-Allow-Origin', '*')
 9        methods = ('Access-Control-Allow-Methods', 'HEAD, OPTIONS, GET, POST, DELETE, PUT')
10        if headers:
11            headers.add(*origin)
12            headers.add(*methods)
13        else:
14            headers = Headers([origin, methods])
15        kwargs['headers'] = headers
16        return super().__init__(response, **kwargs)

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

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

(全文完)