天天看點

跨域實踐背景

背景

最近在 ITA 寫了一個聊天機器人的 Flask 服務,自己寫了一些 node 單元測試腳本跑沒有問題,但是測試的同學也想覆寫到所有的 case,于是就幫忙寫一個 html 頁面去測試,然後就遇到了下面的問題:

XMLHttpRequest cannot load http://localhost:8085/predict. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.

這個是典型的跨域問題(跨域是指:協定、域名、端口有任何一個不同,都被當做是不同的域),想想之前也了解過跨域的知識,現在借着這個機會總結一下了。關于 GET 請求的跨域,使用 JSONP 是目前最好的解決方案,各大浏覽器也基本都支援 JSONP,而 jQuery,AngularJS 等前端架構也都預設添加了對 JSONP 的封裝,并且這次遇到的跨域問題是 POST 請求的,于是暫時先不寫關于 JSONP 的相關知識。

簡化代碼

伺服器代碼:

from flask import Flask
if __name__ == "__main__":
    print('Start server')
    app = Flask(__name__)
    # 路由
    @app.route('/predict', methods=['POST'])
    def predict():
        return 'result'
    app.run(host='0.0.0.0', port=8085, debug=True)           

複制

頁面代碼:

<!doctype html>
<html ng-app="chatApp">
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"></script>
    <script src="./main.js"></script>
</head>
<body>
    <div ng-controller="ChatController">
        <input type="text" ng-model="chat" placeholder="Enter content here">
        <button ng-click="onclick()">POST</button>
        <p> {{ result }} </p>
    </div>
</body>
</html>           

複制

– 原諒我用 Angular 做頁面 ☹

main.js

angular.module('chatApp', [])
    .controller('ChatController', ['$scope', '$http', function($scope, $http) {
        $scope.onclick = function() {
            $http({
                method: 'POST',
                url: 'http://localhost:8085/predict'
            }).then((data) => {
                $scope.result = data;
            });
        };
    }]);           

複制

解決方案

要想解決跨域,必先了解跨域。那什麼是跨域呢?

對于 web 開發來講,由于浏覽器的同源政策,我們需要經常使用一些 hack 的方法去跨域擷取資源,直到 W3C 出了一個标準-CORS-“跨域資源共享”(Cross-origin resource sharing),

它允許浏覽器向跨源伺服器,發出 XMLHttpRequest 請求,進而克服了 AJAX 隻能同源使用的限制。

CORS 與 JSONP 的使用目的相同,但是比 JSONP 更強大。

JSONP 隻支援 GET 請求,CORS 支援所有類型的 HTTP 請求。JSONP 的優勢在于支援老式浏覽器,以及可以向不支援 CORS 的網站請求資料。

CORS 解決方案:

(1) 伺服器代碼

from flask import Flask, Response, request
if __name__ == "__main__":
    print('Start server')
    app = Flask(__name__)
    # post
    @app.route('/predict', methods=['POST'])
    def predict():
        if request.form.get('content') is None:
            exp = 'Missing content'
        else:
            exp = request.form.get('content')
        print(exp)
        headers = {"Access-Control-Allow-Origin": "*"}
        return Response(exp, headers=headers)
    # port=8085
    app.run(host='0.0.0.0', port=8085, debug=True)           

複制

(2) main.js

angular.module('chatApp', [])
    .controller('ChatController', ['$scope', '$http', function($scope, $http) {
        $scope.onclick = function() {
            $http({
                method: 'POST',
                url: 'http://localhost:8085/predict',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                data: 'content= ' + $scope.chat

            }).then((data) => {
                $scope.result = data.data;
            });
        };
    }]);           

複制

此時再次發送 Ajax call就可以拿到結果了:

跨域實踐背景

注意到伺服器端代碼發生了一點改動,那就是在Response header中增加了一個參數 “Access-Control-Allow-Origin”,表示接受某域名的請求,“*” 表示允許所有的請求。

也可以使用确定的值,如: “http://api.abc.com”。

于是代碼中增加 headers = {“Access-Control-Allow-Origin”: ""}* 後伺服器就可以響應所有的請求了。

再看 Web 端的代碼,我們在請求頭裡面添加了 “Content-Type”,為了能向服務端傳遞資料。這裡使用的 “Content-Type” 為 “application/x-www-form-urlencoded” 表示以表單送出的形式傳遞參數。

為什麼要用表單的形式送出POST請求呢?

兩種請求

浏覽器将 CORS 請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

隻要同時滿足以下兩大條件,就屬于簡單請求。

(1) 請求方法是以下三種方法中的一個:
  • HEAD
  • GET
  • POST

    (2) HTTP的頭資訊不超出以下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type 其值僅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain

上文中的請求屬于簡單請求。

簡單請求(simple request)

對于簡單的跨域請求,浏覽器會自動在請求的頭資訊加上 Origin 字段,表示本次請求來自哪個源(協定 + 域名 + 端口),服務端會擷取到這個值,然後判斷是否同意這次請求并傳回。

// 請求

GET /cors HTTP/1.1

Origin: http://api.abc.com

Host: api.bcd.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

如果服務端許可本次請求,就會在傳回的頭資訊多出關于 Access-Control 的資訊,比如上述伺服器傳回的資訊:

跨域實踐背景

非簡單請求(not-so-simple request)

非簡單請求是那種對伺服器有特殊要求的請求,比如請求方法是 PUT 或 DELETE,或者 Content-Type 字段的類型是 application/json。

非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為“預檢”請求(preflight)。

浏覽器先詢問伺服器,目前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些 HTTP 動詞和頭資訊字段。隻有得到肯定答複,浏覽器才會發出正式的 XMLHttpRequest 請求,否則就報錯。

“預檢”請求用的請求方法是 OPTIONS,表示這個請求是用來詢問的。頭資訊裡面,關鍵字段是Origin,表示請求來自哪個源。

非簡單請求解決方案

項目中使用的 Content-Type 為 application/json,屬于非簡單請求,将上述程式修改為

(1) main.js:

angular.module('chatApp', [])
    .controller('ChatController', ['$scope', '$http', function($scope, $http) {
        $scope.onclick = function() {
            $http({
                method: 'POST',
                url: 'http://localhost:8086/predict',
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify({
                    'content': $scope.chat
                })
            }).then((data) => {
                $scope.result = data.data;
            });
        };
    }]);           

複制

伺服器代碼:

from flask import Flask, Response, request

if __name__ == "__main__":
    print('Start server')
    app = Flask(__name__)
    # 路由
    @app.route('/predict', methods=['POST', 'OPTIONS'])
    def predict():
    	# 傳回頭
        headers = {"Access-Control-Allow-Origin": "*",
                   "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type",
                   "Access-Control-Allow-Methods": "POST, PUT, GET, OPTIONS, DELETE"}
        # preflight
        if request.method == 'OPTIONS':
            return Response(headers=headers)
        # request
        if 'content' in request.json:
            exp = request.json.get('content')
        else:
            exp = 'Missing content'
        print(exp)
        return Response(exp, headers=headers)
    # run server
    app.run(host='0.0.0.0', port=8086, debug=True)           

複制

啟動後發送請求,發現可以跑通,但是擷取不到參數,原因是使用 application/json 的形式發送 request, 參數并沒有放在 form 裡面,而是放在 request.data 裡面了。

request.data 裡面為 bytes 類型的資料,通過 request.json 可以擷取其 dict 類型。

通過以上方式,完美地解決了複雜請求的跨域問題。

才怪嘞!!!

問題所在

以上解決跨域的方式為 CORS,準确地說,這是一種伺服器端的技術。而現實生産環境中,如果一個前端想要用這種方式實作跨域,不知道要跟後端做多少溝通,那有沒有純前端的解決方案呢?

且聽下回分解。