06月19, 2018

利用 CORS 方式解决跨域问题(Flask 实现)

跨域问题是在 web 开发中经常遇到的一个问题。常见的解决方案网上罗列了很多,据了解,大家目前常用的是 JSONP 的方式。但这种方式只能使用 GET 方式,存在一定的局限性。所以未来更多的跨域场景,应该会使用 CORS 的方式实现。这里使用 Flask 框架作为服务端,模拟跨域场景,并使用 CORS 方式解决所遇到的跨域问题。

CORS 的具体概念,可以参考MDN和阮一峰博客的相关文章(在这里这里),本文不再花费文字赘述。这里直接给出代码示例和讲解。

首先,我们要创建两个服务端,模仿实际场景中一个作为页面自身的服务端,我们称为 服务器A,一个作为被请求数据的服务端,我们称为 服务器B。代码其实一样,唯一区别是启动时所填写的 host 不同。

import json  
from functools import wraps

from flask import Flask, make_response, render_template, request

app = Flask(__name__)


def cors(func):  
    @wraps(func)
    def wrapper_func(*args, **kwargs):
        r = make_response(func(*args, **kwargs))
        r.headers['Access-Control-Allow-Origin'] = '*'
        r.headers['Access-Control-Allow-Methods'] = 'HEAD, OPTIONS, GET, POST, DELETE, PUT'
        allow_headers = "Referer, Accept, Origin, User-Agent, X-Requested-With, Content-Type"
        r.headers['Access-Control-Allow-Headers'] = allow_headers
        return r

    return wrapper_func


@app.route('/cors', methods=['POST', 'GET'])
@cors
def domains():  
    return json.dumps({'data': 'data from A by {}'.format(request.method)})


@app.route('/')
def main():  
    return render_template('index.html')


if __name__ == '__main__':  
    app.run(host='localhost', port=8080)

上面是 服务器A 的具体代码

import json  
from functools import wraps

from flask import Flask, make_response, render_template, request

app = Flask(__name__)


def cors(func):  
    @wraps(func)
    def wrapper_func(*args, **kwargs):
        r = make_response(func(*args, **kwargs))
        r.headers['Access-Control-Allow-Origin'] = '*'
        r.headers['Access-Control-Allow-Methods'] = 'HEAD, OPTIONS, GET, POST, DELETE, PUT'
        allow_headers = "Referer, Accept, Origin, User-Agent, X-Requested-With, Content-Type"
        r.headers['Access-Control-Allow-Headers'] = allow_headers
        return r

    return wrapper_func


@app.route('/cors', methods=['POST', 'GET'])
@cors
def domains():  
    return json.dumps({'data': 'data from B by {}'.format(request.method)})


@app.route('/')
def main():  
    return render_template('index.html')


if __name__ == '__main__':  
    app.run(host='127.0.0.1', port=8088)

上面是 服务器B 的具体代码,可以看到,除了 host 不同之外,没有任何区别。

接下来我们创建页面:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">
    <title>CORS</title>
</head>  
<body>  
<input id="id-input-url">  
<button class="button-send" data-method="GET">get</button>  
<button class="button-send" data-method="POST">post</button>  
<script>  
    var cors = function (method) {
        var url = document.getElementById('id-input-url')
        const xhr = new XMLHttpRequest()
        xhr.open(method, url.value, true)
        if (method.toUpperCase() === 'POST') {
            // set this if the method is POST
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
        }
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                alert(xhr.responseText)
            }
        }
        xhr.send()
    }
    var button = document.getElementsByClassName('button-send')
    var bl = button.length
    for (var i = 0; i < bl; i++) {
        button[i].addEventListener('click', function () {
            cors(this.dataset.method)
        })
    }
</script>  
</body>  
</html>  

可以看到,页面中 Javascript 代码其实与我们平常所写的 ajax 请求过程没有什么区别。因为在 CORS 中,跨域问题其实主要是由服务端和浏览器完成,对于前端开发人员来讲,与普通不跨域的场景相比其实没有什么区别。

使用 python3 a.pypython3 b.py 启动两个服务器代码,然后访问 http://localhost:8080,在页面中 input 元素内填写 http://127.0.0.1:8088/cors,并点击 GET 按钮,测试一下,是否正常的弹出了警告框,显示 {'data': 'data from B by GET'}。当然,POST 按钮得到的结果也是一样,只是最后的 GET 被替换成了 POST

那么这里我们就已经实现了针对 GET方法POST方法 的跨域。那么在某些情况下,可能在跨域时,需要提交 cookie 信息,怎么办呢?

其实很简单,只要在服务端的 cors 装饰器中,设置响应头时,Access-Control-Allow-Origin 字段填写对方的完整 url,并且增加 Access-Control-Allow-Credentials 字段,设置为 'true'。然后在页面中,xhr 对象 send 前,增加 xhr.withCredentials = true 即可。具体修改部分的代码如下:

def cors(func):  
    @wraps(func)
    def wrapper_func(*args, **kwargs):
        r = make_response(func(*args, **kwargs))
        r.headers['Access-Control-Allow-Origin'] = 对方的完整 url
        r.headers['Access-Control-Allow-Methods'] = 'HEAD, OPTIONS, GET, POST, DELETE, PUT'
        allow_headers = "Referer, Accept, Origin, User-Agent, X-Requested-With, Content-Type"
        r.headers['Access-Control-Allow-Headers'] = allow_headers
        # if you need the cookie access, uncomment this line
        r.headers['Access-Control-Allow-Credentials'] = 'true'
        return r

    return wrapper_func
var cors = function (method) {  
    var url = document.getElementById('id-input-url')
    const xhr = new XMLHttpRequest()
    xhr.open(method, url.value, true)
    xhr.withCredentials = true
    if (method.toUpperCase() === 'POST') {
        // set this if the method is POST
        // IF YOU WANT TO POST WITH COOKIE
        // IT MUST BE "application/x-www-form-urlencoded"
        // DO NOT CHANGE THIS VALUE !!!!
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
    }
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            alert(xhr.responseText)
        }
    }
    xhr.send()
}

如此一来,即可实现带 Cookie 的跨域了。

有几个需要注意的地方:

  1. cors 装饰器中,填写对方的完整 url时,一定要写明协议,http://https://。如果端口号不同,一定要带上端口号,并且不要添加最后一个 /,举个例子,应该这样写:http://127.0.0.1:8088
  2. Javascript 中,使用 POST 带 Cookie 跨域时,Content-type 一定要设置成 application/x-www-form-urlencoded,否则还是会跨域失败。

本文链接:http://pycode.cc/post/solve-cross-domain-by-cors-in-flask.html

-- EOF --

Comments