要提供一个 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.
从当时请求的内容(见下方)可以看出,请求变成了 OPTION 而不是 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 请求。
我们可以通过自定义的 Response 类(作为 Flask Response 的子类)来实现这个需求,让 flask 回复的任何响应都带有 Access-Control-Allow-*
的 HEAD。通过设置 Flask app 的 response_class
属性可以做到这点。
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)
了解了 __init__
的结构,我们可以这样做继承。由于参数太多,我们仅保留一个 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 。
(全文完)