第二章 Flask與HTTP(上)

? ??在第1章,我們已經了解了Flask的基本知識,如果想要進一步開發更復雜的Flask應用,我們就得了解Flask與HTTP協議的交互方式。HTTP(Hypertext Transfer Protocol,超文本傳輸協議)定義了服務器和客戶端之間信息交流的格式和傳遞方式,它是萬維網(World Wide Web)中數據交換的基礎。

? ??在這一章,我們會了解Flask處理請求和響應的各種方式,并對HTTP協議以及其他非常規HTTP請求進行簡單的介紹。雖然本章的內容很重要,但鑒于內容有些晦澀難懂,如果感到困惑也不用擔心,本章介紹的內容你會在后面的實踐中逐漸理解和熟悉。如果你愿意,也可以臨時跳過本章,等到學習完本書第一部分再回來重讀。

? ??HTTP的詳細定義在RFC 7231~7235中可以看到。RFC(Request For Comment,請求評議)是一系列關于互聯網標準和信息的文件,可以將其理解為互聯網(Internet)的設計文檔。完整的RFC列表可以在這里看到:https://tools.ietf.org/rfc/

2.1 請求響應循環

1.jpg

? ??有一個類似我們第1章編寫的程序運行著。它負責接收用戶的請求,并把對應的內容返回給客戶端,顯示在用戶的瀏覽器上。事實上,每一個Web應用都包含這種處理模式,即“請求-響應循環(Request-Response Cycle)”:客戶端發出請求,服務器端處理請求并返回響應。

image.png

? ??當用戶訪問一個URL,瀏覽器便生成對應的HTTP請求,經由互聯網發送到對應的Web服務器。Web服務器接收請求,通過WSGI將HTTP格式的請求數據轉換成我們的Flask程序能夠使用的Python數據。在程序中,Flask根據請求的URL執行對應的視圖函數,獲取返回值生成響應。響應依次經過WSGI轉換生成HTTP響應,再經由Web服務器傳遞,最終被發出請求的客戶端接收。瀏覽器渲染響應中包含的HTML和CSS代碼,并執行JavaScript代碼,最終把解析后的頁面呈現在用戶瀏覽器的窗口中。

2.2 HTTP請求

? ??URL是一個請求的起源。不論服務器在何地運行,當我們輸入指向服務器所在的地址的URL,都會向服務器發送一個HTTP請求。一個標準的URL由很多部分組成,以下面這個URL為例:

? ????http://helloflask.com/hello?name=Grey

? ??這個url的各個組成部分如表所示:

信息 說明
http:// 協議字符串,指定要使用的協議
helloflask.com 服務器的地址(域名)
/hello?name=Grey 要獲取的資源路徑(path),類似UNIX的文件目錄結構

這個URL后面的?name=Grey部分是查詢字符串(query string)。URL中的查詢字符串用來向指定的資源傳遞參數.查詢字符串從?開始,以鍵值對的形式寫出,多個鍵值對之間用&分隔

2.2.1 請求報文

? ??當我們在瀏覽器中訪問這個URL時,隨之產生的是一個發向http://helloflask.com所在服務器的請求。請求的實質是發送到服務器上的一些數據,這種瀏覽器與服務器之間交互的數據成為報文(message),請求時瀏覽器發送的數據稱為請求報文(request message),而服務器返回的數據稱為響應報文(response message).

? ??請求報文有請求的方法.URL.協議版本.首部字段(header)以及內容實體組成.前面的請求產生的請求報文示意表如下所示:

3.jpg

? ??如果你想看真實的HTTP報文,可以在瀏覽器中向任意一個有效的URL發起請求,然后在瀏覽器的開發者工具(F12)里的Network標簽中看到URL對應資源加載的所有請求列表,單擊任一個請求條目即可看到報文信息,如下所示:

4.jpg

? ??報文由報文首部和報文主體組成,兩者由空行分隔,請求報文的主體一般為空.如果URL中包含查詢字符串,或者是提交了表單,name報文主體將會是查詢字符串和表單數據.

? ??HTTP通過方法來區分不同的請求類型,比如,當你直接訪問一個頁面時,請求的方法是GET;當你在某個頁面填寫了表單并提交時,請求方法則通常為POST.

方法 說明 方法 說明
GET 獲取資源 DELETE 刪除資源
POST 傳輸數據 HEAD 獲得報文首部
PUT 傳輸文件 OPTIONS 詢問支持的方法

? ??報文首部包含了請求的各種信息,比如客戶端類型、是否設置緩存、語言偏好等。

HTTP中可用的首部字段列表可以在https://www.iana.org/assignments/message-headers/message-headers.xhtml 看到。請求方法的詳細列表和說明可以在RFC 7231(https://tools.ietf.org/html/rfc7231 )中看到。

如果運行了示例程序,那么當你在瀏覽器中訪問http://127.0.0.1:5000/hello 時,開發服務器會在命令行中輸出一條記錄日志,其中包含請求的主要信息:

127.0.0.1 - - [02/Aug/2017 09:51:37] "GET /hello HTTP/1.1" 200 –

2.2.2 Request對象

? ??假設請求的url是:http://helloflask.com/hello?name=Grey

? ??使用request的屬性獲取獲取請求url屬性如下:

屬性 屬性
path u'/hello' base_url u'http://helloflask.com/hello'
full_path u'/hello?name=Grey' url u'http://helloflask.com/hello?name=Grey'
host u'helloflask.com url_root u'http://helloflask.com/'
host_url u'http://helloflask.com/'

? ??request對象常用的屬性和方法:

5.jpg

? ??Werkzeug的MutliDict類是字典的子類,它主要實現了同一個鍵對應多個值的情況。比如一個文件上傳字段可能會接收多個文件。這時就可以通過getlist()方法來獲取文件對象列表。而ImmutableMultiDict類繼承了MutliDict類,但其值不可更改。更多內容可訪問Werkzeug相關數據結構章節http://werkzeug.pocoo.org/docs/latest/datastructures/

? ??代碼實例2-1:

??獲取請求URL中的查詢字符串

from flask import Flask, request

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'Flask')    # 獲取查詢參數name的值
    return '<h1>Hello, %s!<h1>' % name            

? ??訪問:http://localhost:5000/hello?name=Grey

? ??輸出:Hello, Grey!

? ??上面的示例代碼包含安全漏洞,在現實中我們要避免直接將用戶傳入的數據直接作為響應返回,在本章的末尾我們將介紹這個安全漏洞的具體細節和防范措施。

? ??需要注意的是,和普通的字典類型不同,當我們從request對象的類型為MutliDict或ImmutableMultiDict的屬性(比如files、form、args)中直接使用鍵作為索引獲取數據時(比如request.args['name']),如果沒有對應的鍵,那么會返回HTTP 400錯誤響應(Bad Request,表示請求無效),而不是拋出KeyError異常,如圖所示。為了避免這個錯誤,我們應該使用get()方法獲取數據,如果沒有對應的值則返回None;get()方法的第二個參數可以設置默認值,比如requset.args.get('name','Human')。

6.jpg

如果開啟了調試模式,那么拋出BadRequestKeyError異常并顯示對應的錯誤棧信息,而不是常規的404錯誤

2.2.3 在flask中處理請求

? ??URL是指網絡上資源的地址。在Flask中,我們需要讓請求的URL匹配對應的視圖函數,視圖函數返回值就是URL對應的資源。

1.路由匹配

? ??為了便于將請求分發到對應的函數,程序實例存儲了一個路由表(app.url_map),其中定義了URL規則和視圖函數的映射關系,當請求發來后,Flask會根據請求報文中的URL(path部分)來嘗試與這個表中的所有URL規則進行匹配,調用匹配成功的視圖函數.如果沒有匹配的URL規則,說明程序中沒有處理這個URL的視圖函數,flask]會自動返回404錯誤響應(Not Found表示資源未找到),你可以嘗試在瀏覽器中訪問http://localhost:5000/nothing ,因為我們的程序中沒有視圖函數負責處理這個URL,所以你會得到404響應。

? ??當請求的URL與某個視圖函數的URL規則匹配成功時,對應的視圖函數就會調用。使用flask routes命令可以查看程序中定義的所有路由,這個列表由app.url_map解析得到:

? ??$ flask routes
? ??Endpoint Methods Rule


? ??hello_world GET /hello
? ??static GET /static/<path:filename>

? ??在輸出的文本中,我們看到每個路由對應斷點(Endpoint)、HTTP方法(Methods)和URL規則(Rule),其中static是flask添加的特殊路由,用來訪問靜態文件

2.設置監聽的HTTP方法

? ??通過flask routes命令打印出的路由列表可以看到,每一個路由除了包含URL規則外,還設置了監聽的HTTP方法。GET是最常用的HTTP方法,所以視圖函數默認監聽的方法類型就是GET,HEAD、OPTIONS方法的請求由Flask處理,而像DELETE、PUT等方法一般不會在程序中實現,在后面我們構建Web API時才會用到這些方法。

? ??我們可以在app.route()裝飾器中使用methods參數傳入一個包含監聽的HTTP方法的可迭代對象。比如,下面的視圖函數同時監聽GET請求和POST請求:

@app.route('/hello', methods=['GET', 'POST'])
def hello():
    return "<h1>hello, Flask!</h1>"

? ??當某個請求的方法不合符要求時,請求將無法被正常處理。比如,提交表單通常使用POST方法,而提交的目標URL對應的視圖函數只允許使用GET方法,這是Flask會自動返回一個405錯誤響應(Method Not Allowed, 表示請求方法不允許),如圖所示:


image.png

? ??通過定義方法列表,我們可以為同一個URL規則定義多個視圖函數,分別處理不同HTTP方法的請求。

3.URL處理

? ??從前面的路由列表可以看到,除了/hello,這個程序還包含許多URL規則,比如和go_back端點對應的/goback/<int:year>。現在請嘗試訪問http://localhost:5000/goback/34 ,在URL中加入一個數字作為時光倒流的年數,你會發現加載后的頁面中有通過傳入的年數計算出的年份:“Welcome to 1984!”。仔細觀察一下,你會發現URL規則中的變量部分有一些特別,<int:year>表示為year變量添加了一個int轉換器,Flask在解析這個URL變量時會將其轉換為整型。URL中的變量部分默認類型為字符串,但Flask提供了一些轉換器可以在URL規則里使用,如下表所示:

??Flask內置的URL變量轉換器

轉換器 說明
string 不包含斜線的字符串(默認值)
int 整型
float 浮點數
path 包含斜線的字符串.static路由的URL規則中的filename變量就使用了這個轉換器
any 匹配一系列給定值中的一個元素
uuid UUID字符串

? ??轉換器通過特定的規則指定,即"<轉換器:變量名>"。"<int: year>"把year的值轉換為整數,因此我們可以在視圖函數中直接對year變量進行科學計算:

@app.route('goback/<int:year>')
def go_back(year):
    return '<p>Welcome to %d!</p>' % (2018 - year)

? ??默認的行為不僅僅是類型轉換,還包括URL匹配。在這個例子中,如果不使用轉換器,默認year變量會被轉換成字符串,為了能夠在Python中計算天數,我們需要使用int()函數將year變量轉換成整型。但是如果用戶輸入的是英文字母,就會出現轉換錯誤,拋出ValueError異常,我們還需要手動驗證;使用了轉換器后,如果URL中傳入的變量不是數字,那么會直接返回404錯誤響應。比如,你可以嘗試訪問http://localhost:5000/goback/tang

? ??在用法上唯一特別的是any轉換器,你需要在轉換器后添加括號來給出可選值,即"<any(value1,valuel2,...):變量名>",比如:

@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
    return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'

? ??當你在瀏覽器中訪問http://localhost:5000/colors/ 時,如果將<color>部分替換為any轉換器中設置的可選值以外的任意字符,均會獲得404錯誤響應。

? ??如果你想在any轉換器中傳入一個預先定義的列表,可以通過格式化字符串的方式(使用%或是format()函數)來構建URL規則字符串,比如:

colors = ['blue', 'white', 'red']

@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])
...

2.2.4 請求鉤子

? ??有時我們需要對請求進行預處理(preprocessing)和后處理(postprocessing),這時可以使用Flask提供的一些請求鉤子(Hook),他們可以用來注冊在請求處理的不同階段執行的處理函數(或稱為回調函數,即Callback)。這些請求鉤子使用裝飾器實現,通過程序實例app調用,用法很簡單:以before_request鉤子(請求之前)為例,當你對一個函數附加了app.before_request裝飾器后,就會將這個函數注冊為before_request處理函數,每次執行請求前都會觸發所有before_request處理函數。Flask默認實現的五種請求鉤子如下表所示:

請求鉤子

鉤子 說明
before_first_request 注冊一個函數,在處理第一個請求運行
before_request 注冊一個函數,在處理每個請求前運行
after_request 注冊一個函數,如果沒有未處理的異常拋出,會在每個請求結束后運行
teardown_request 注冊一個函數,即使有未處理的異常拋出,會在每個請求結束后運行。如果發生異常,會傳入異常對象作為參數到注冊的函數中
after_this_request 在視圖函數內注冊一個函數,會在這個請求結束后運行

? ??這些鉤子使用起來和app.route()裝飾器基本相同,每個鉤子可以注冊多個處理函數,函數名并不是必須和鉤子名稱相同,下面是一個基本實例:

@app.before_request
def do_something():
    pass # 這里的代碼會在每個請求處理前執行

? ??假如我們創建了三個視圖函數A、B、C,其中視圖C使用了after_this_reques鉤子,那么當請求A進入后,整個請求處理周期的請求處理函數調用流程如下所示。

? ??請求鉤子常用場景:

·before_first_request:在玩具程序中,運行程序前我們需要進行一些程序的初始化操作,比如創建數據庫表,添加管理員用戶。這些工作可以放到使用before_first_request裝飾器注冊的函數中。

·before_request:比如網站上要記錄用戶最后在線的時間,可以通過用戶最后發送的請求時間來實現。為了避免在每個視圖函數都添加更新在線時間的代碼,我們可以僅在使用before_request鉤子注冊的函數中調用這段代碼。

·after_request:我們經常在視圖函數中進行數據庫操作,比如更新、插入等,之后需要將更改提交到數據庫中。提交更改的代碼就可以放到after_request鉤子注冊的函數中。

另一種常見的應用是建立數據庫連接,通常會有多個視圖函數需要建立和關閉數據庫連接,這些操作基本相同。一個理想的解決方法是在請求之前(before_request)建立連接,在請求之后(teardown_request)關閉連接。通過在使用相應的請求鉤子注冊的函數中添加代碼就可以實現。這很像單元測試中的setUp()方法和tearDown()方法。

請求鉤子流程圖
請求鉤子流程圖

注意

? ??after_request鉤子和after_this_request鉤子必須接收一個響應類對象作為參數,并且返回同一個或更新后的響應對象。

2.3 HTTP響應

? ??在Flask程序中,客戶端發出的請求觸發響應的視圖函數,獲取返回值會作為響應的主體最后生成完整的響應,即響應報文

2.3.1 響應報文

? ??響應報文主要由協議版本、狀態碼(status code)、原因短語(reason phrase)、響應首部和響應主體組成。以發向localhost:5000/hello的請求為例,服務器生成的響應報文示意如圖所示

響應報文

響應報文

? ??響應報文的首部包含一些關于響應和服務器的信息,這些內容由Flask生成,而我們在視圖函數中返回的內容即為響應報文中的主體內容。瀏覽器接收到響應后,會把返回的響應主體解析并顯示在瀏覽器窗口上。

? ??HTTP狀態碼用來表示請求處理的結果,下表是常見的幾種狀態碼和相應的原因短語。

常見的HTTP狀態碼

常見的HTTP狀態碼

當關閉調試模式,即FLASK_ENV使用默認值production,如果程序出錯,Flask會自動返回500錯誤響應,而調試模式下則會顯示調試信息和錯誤堆棧

響應狀態碼的詳細列表和說明可以在RFC7321(https://tools.ietf.org/html/rfc7231 )中看到

2.3.2在Flask中生成響應

? ??響應在Flask中使用Response對象表示,響應報文中的大部分內容由服務器處理,大多數情況下,我們只負責返回主體內容。

? ??根據我們在上一節介紹的內容,Flask會先判斷是否可以找到與請求URL相匹配的路由,如果沒有則返回404響應。如果找到,則調用對應的視圖函數,視圖函數的返回值構成了響應報文的主體內容,正確返回時狀態碼默認為200。Flask會調用make_response()方法將視圖函數返回值轉換為響應對象。

? ??完整地說,視圖函數可以返回最多由三個元素組成的元組:響應主體、狀態碼、首部字段。其中首部字段可以為字典,或是兩元素元組組成的列表。

? ??比如,普通的響應可以只包含主體內容:

@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>'

? ??默認的狀態碼為200,下面指定了不同的狀態碼:

@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>', 201

? ??有時你會想附加或修改某個首部字段。比如,要生成狀態碼為3XX的重定向響應,需要將首部中的Location字段設置為重定向的目標URL:

? ```

@app.route('/hello')
def hello():
    ...
    return '', 302, {'Location', 'http://www.example.com'}

? ??現在訪問http://localhost:5000/hello ,會重定向到http://www.example.com 。在多數情況下,除了響應主體,其他部分我們通常只需要使用默認值即可。

1.重定向

? ??如果你訪問http://localhost:5000/hi ,你會發現頁面加載后地址欄中的URL變為了http://localhost:5000/hello 。這種行為被稱為重定向(Redirect),你可以理解為網頁跳轉。在上一節的示例中,狀態碼為302的重定向響應的主體為空,首部中需要將Location字段設為重定向的目標URL,瀏覽器接收到重定向響應后會向Location字段中的目標URL發起新的GET請求,整個流程如圖所示。

重定向流程示意圖

重定向流程示意圖

? ??在Web程序中,我們經常需要進行重定向。比如,當某個用戶在沒有經過認證的情況下訪問需要登錄后才能訪問的資源,程序通常會重定向到登錄頁面。

? ??對于重定向這一類特殊響應,Flask提供了一些輔助函數。除了像前面那樣手動生成302響應,我們可以使用Flask提供的redirect()函數來生成重定向響應,重定向的目標URL作為第一個參數。前面的例子可以簡化為:

from flask import Flask, redirect
# ...
@app.route('/hello')
def hello():
    return redirect('http://www.example.com')

? ??使用redirect()函數時,默認的狀態碼為302,即臨時重定向。如果你想修改狀態碼,可以在redirect()函數中作為第二個參數或使用code關鍵字傳入。

? ??如果要在程序內重定向到其他視圖,那么只需在redirect()函數中使用url_for()函數生成目標URL即可,如下代碼所示。

重定向到其他的視圖

from flask import Flask, redirect, url_for 
...
@app.route('/hi')
def hi():
    ...
    return redierct(url_for('hello'))  # 重定向到/hello

@app.route('/hello')
def hello():
    ...

2.錯誤響應

? ??如果你訪問http://localhost:5000/brew/coffee ,會獲得一個418錯誤響應(I'm a teapot),如圖下圖所示。

418錯誤響應

418錯誤響應

? ??418錯誤響應由IETF(Internet Engineering Task Force,互聯網工程任務組)在1998年愚人節發布的HTCPCP(Hyper Text Coffee Pot Control Protocol,超文本咖啡壺控制協議)中定義(玩笑),當一個控制茶壺的HTCPCP收到BREW或POST指令要求其煮咖啡時應當回傳此錯誤。

? ??大多數情況下,Flask會自動處理常見的錯誤響應。HTTP錯誤對應的異常類在Werkzeug的werkzeug.exceptions模塊中定義,拋出這些異常即可返回對應的錯誤響應。如果你想手動返回錯誤響應,更方便的方法是使用Flask提供的abort()函數。

? ??在abort()函數中傳入狀態碼即可返回對應的錯誤響應,下面代碼中的視圖函數返回404錯誤響應。

返回404錯誤響應

from flask import Flask, abort
...
@app.route('/404')
def not_found():
    abort(404)

abort()函數前不需要使用return語句,但一旦abort()函數被調用,abort()函數之后的代碼將不會被執行。

? ??雖然我們有必要返回正確的狀態碼,但這并不是必須的。比如,當某個用戶沒有權限訪問某個資源時,返回404錯誤要比403錯誤更加友好

2.3.3 響應格式

? ??在HTTP響應中,數據可以通過多種格式傳輸。大多數情況下,我們會使用HTML格式,這也是Flask中的默認設置。在特定的情況下,我們也會使用其他格式。不同的響應數據格式需要設置不同的MIME類型,MIME類型在首部的Content-Type字段中定義,以默認的HTML類型為例:

Content-Type: text/html; charset=utf-8

? ??MIME類型(又稱為media type或content type)是一種用來標識文件類型的機制,它與文件擴展名相對應,可以讓客戶端區分不同的內容類型,并執行不同的操作。一般的格式為“類型名/子類型名”,其中的子類型名一般為文件擴展名。比如,HTML的MIME類型為“text/html”,png圖片的MIME類型為“image/png”。完整的標準MIME類型列表可以在這里看到:https://www.iana.org/assignments/media-types/media-types.xhtml

? ??如果你想使用其他MIME類型,可以通過Flask提供的make_response()方法生成響應對象,傳入響應的主體作為參數,然后使用響應對象的mimetype屬性設置MIME類型,比如:

from flask import make_response

@app.route('/foo')
def foo():
    response = make_response('Hello, World!')
    response.mimetype = 'text/plain'
    return response

? ??你也可以直接設置首部字段,比如response.headers['Content-Type']='text/xml;charset=utf-8'。但操作mimetype屬性更加方便,而且不用設置字符集(charset)選項。

2.3.4 Cookie

? ??HTTP是無狀態(stateless)協議。也就是說,在一次請求響應結束后,服務器不會留下任何關于對方狀態的信息。但是對于某些Web程序來說,客戶端的某些信息又必須被記住,比如用戶的登錄狀態,這樣才可以根據用戶的狀態來返回不同的響應。為了解決這類問題,就有了Cookie技術。Cookie技術通過在請求和響應報文中添加Cookie數據來保存客戶端的狀態信息。

? ??Cookie指Web服務器為了存儲某些數據(比如用戶信息)而保存在瀏覽器上的小型文本數據。瀏覽器會在一定時間內保存它,并在下一次向同一個服務器發送請求時附帶這些數據。Cookie通常被用來進行用戶會話管理(比如登錄狀態),保存用戶的個性化信息(比如語言偏好,視頻上次播放的位置,網站主題選項等)以及記錄和收集用戶瀏覽數據以用來分析用戶行為等。

? ??在Flask中,如果想要在響應中添加一個cookie,最方便的方法是使用Response類提供的set_cookie()方法。要使用這個方法,我們需要先使用make_response()方法手動生成一個響應對象,傳入響應主體作為參數。這個響應對象默認實例化內置的Response類。下表是內置的Response類常用的屬性和方法。

Response類常用的屬性和方法

方法/屬性 說明
headers 一個Werkzeug的Headers對象,表示響應首部,可以像字典一樣操作
status 狀態碼.文本類型
status_code 狀態碼,整形
mimetype MIME類型(僅包括內容類型部分)
set_cookie() 用來設置一個cookie

? ??set_cookie()方法支持多個參數來設置Cookie的選項,如下表所示

set_cookie()方法的參數

屬性 說明
key cookie的鍵(名稱)
value cookie的值
max_age cookie被保存的時間數,單位為秒;默認在用戶會話結束(即關閉瀏覽器)時過期
expires 具體的過期時間,一個datetime對象或UNIX時間戳
path 限制cookie只在給定的路徑可用,默認為整個域名
domain 設置cookie可用的域名
secure 如果設置為True,只有通過HTTPS才可以使用
httponly 如果設置為True,禁止客戶端JavaScript獲取cookie

? ??set_cookie視圖用來設置cookie,他會將URL中的name變量的值設置到name的cookie里,代碼如下所示:

設置cookie

from flask import Flask, make_response
...
@app.route('/set/<name>')
def set_cookie(name):
    response = make_response(redirect(url_for('hello')))
    response.set_cookie('name', name)
    return response

from flask imoprt Flask,make_response
@app.route('/cookie')
def set_cookie():
    resp = make_response('this is to set cookie')
    resp.set_cookie('username', 'itcast')
    return resp

? ??這個make_response()函數中,我們傳入的是使用redirect()函數生成的重定向響應。set_cookie視圖會在生成的響應報文首部中創建一個Set-Cookie字段,即“Set-Cookie:name=Grey;Path=/”。

? ??現在我們查看瀏覽器中的Cookie,就會看到多了一塊名為name的cookie,其值為我們設置的“Grey”,如下圖所示。因為過期時間使用默認值,所以會在瀏覽會話結束時(關閉瀏覽器)過期。

在瀏覽器中查看cookie

image.png

? ??當瀏覽器保存了服務端設置的cookie后,瀏覽器再次發送到該服務器的請求會自動攜帶設置的Cookie信息,Cookie的內容存儲在請求首部的Cookie字段中,整個交互過程由上至下如下圖所示:

Cookie設置示意圖

9.jpg

? ??在Flask中,Cookie可以通過請求對象的cookie屬性讀取,在修改后的hello視圖中,如果沒有從查詢參數中獲取到name的值,就會從Cookie中尋找:

from flask import Flask, request

@app.route('/')
@app.route('/hello')
def hello():
    name = request.args.get('name')
    if name is None:
        name = request.cookies.get('name', 'Human')  # 從Cookie中獲取name值
    return '<h1>Hello, %s</h1>' % name

? ??這時服務器就可以根據Cookie的內容來獲得客戶端的狀態信息,并根據狀態返回不同的響應。如果你訪問http://localhost:5000/set/Grey ,那么就會將名為name的cookie設為Grey,重定向到/hello后,你會發現返回的內容變成了“Hello,Grey!”。如果你再次通過訪問http://localhost:5000/set/ 修改name cookie的值,那么重定向后的頁面返回的內容也會隨之改變。

2.3.5 session:安全的Cookie

? ??當我們使用瀏覽器登錄某個社交網站時,會在登錄表單中填寫用戶名和密碼,單擊登錄按鈕后,這會向服務器發送一個包含認證數據的請求。服務器接收請求后會查找對應的賬戶,然后驗證密碼是否匹配,如果匹配,就在返回的響應中設置一個cookie,比如,“login_user:greyli”。

? ??響應被瀏覽器接收后,cookie會被保存在瀏覽器中。當用戶再次向這個服務器發送請求時,根據請求附帶的Cookie字段中的內容,服務器上的程序就可以判斷用戶的認證狀態,并識別出用戶。

? ??但是這會帶來一個問題,在瀏覽器中手動添加和修改Cookie是很容易的事,僅僅通過瀏覽器插件就可以實現。所以,如果直接把認證信息以明文的方式存儲在Cookie里,那么惡意用戶就可以通過偽造cookie的內容來獲得對網站的權限,冒用別人的賬戶。為了避免這個問題,我們需要對敏感的Cookie內容進行加密。方便的是,Flask提供了session對象用來將Cookie數據加密儲存。

? ??在編程中,session指用戶會話(user session),又稱為對話(dialogue),即服務器和客戶端/瀏覽器之間或桌面程序和用戶之間建立的交互活動。在Flask中,session對象用來加密Cookie。默認情況下,它會把數據存儲在瀏覽器上一個名為session的cookie里。

1.設置程序密鑰

? ??session通過密鑰對數據進行簽名以加密數據,因此,我們得先設置一個密鑰.這里的密鑰就是一個局喲偶一定復雜度和隨機性的字符串,比如"ADSFFVUKJYHTGRD".

? ??程序的密鑰可以通過Flask.secret_key屬性或配置變量SECRET_KEY設置,比如:

app.secret_key = 'secret string'

? ??更安全的做法是把密鑰寫進系統環境變量(在命令行中使用export或set命令),或者保存在.env文件中:

? SECRET_KEY = secret string

? ??然后在程序腳本中使用os模塊提供的getenv()方法獲取:

import os
# ...
app.secret_key = os.getenv('SECRET_KEY', 'secret string')

? ??我們可以在getenv()方法中添加第二個參數,作為沒有獲取到對應環境變量時使用的默認值。

這里的密鑰只是示例。在生產環境中,為了安全考慮,你必須使用隨機生成的密鑰

2.模擬用戶認證

? ??下面我們會使用session模擬用戶的認證功能。

登入用戶

from flask import redirect, session, url_for

@app.route('/login')
def login():
    session['logged_in'] = True  # 寫入session
    return redirect(url_for('hello'))

? ??這個登錄視圖只是簡化的示例,在實際的登錄中,我們需要在頁面上提供登錄表單,供用戶填寫賬戶和密碼,然后在登錄視圖里驗證賬戶和密碼的有效性。session對象可以像字典一樣操作,我們向session中添加一個logged-in cookie,將它的值設為True,表示用戶已認證。

? ??當我們使用session對象添加cookie時,數據會使用程序的密鑰對其進行簽名,加密后的數據存儲在一塊名為session的cookie里,如下圖所示。

? ??你可以在下圖方框內的Content部分看到對應的加密處理后生成的session值。使用session對象存儲的Cookie,用戶可以看到其加密后的值,但無法修改它。因為session中的內容使用密鑰進行簽名,一旦數據被修改,簽名的值也會變化。這樣在讀取時,就會驗證失敗,對應的session值也會隨之失效。所以,除非用戶知道密鑰,否則無法對session cookie的值進行修改。

image.png

? ??當支持用戶登錄后,我們就可以根據用戶的認證狀態分別顯示不同的內容。在login視圖的最后,我們將程序重定向到hello視圖,下面是修改后的hello視圖

@app.route('/')
@app.route('/hello')
def hello():
    name = request.args.get('name')
    if name is None:
        name = request.cookies.get('name', 'Human')
    response = '<h1>Hello, %s!</h1>' % name
        # 根據用戶認證狀態返回不同的內容
    if 'logged_in' in session:
        response += '[Authenticated]'
    else:
        response += '[Not Authenticated]'
    return response

? ??session中的數據可以像字典一樣通過鍵讀取,或是使用get()方法。這里我們只是判斷session中是否包含logged_in鍵,如果有則表示用戶已經登錄。通過判斷用戶的認證狀態,我們在返回的響應中添加一行表示認證狀態的信息:如果用戶已經登錄,顯示[Authenticated];否則顯示[Not authenticated]。

??如果你訪問http://localhost:5000/login ,就會登入當前用戶,重定向到http://localhost:5000/hello 后你會發現加載后的頁面顯示一行“[Authenticated]”,表示當前用戶已經通過認證,如下圖所示。

已認證主頁

已認證主頁

? ??程序中的某些資源僅提供給登入的用戶,比如管理后臺,這時我們就可以通過判斷session是否存在logged_in鍵來判斷用戶是否認證,下面的代碼是模擬管理后臺的admin視圖

模擬管理后臺

from flask import session, abort

@app.route('/admin')
def admin():
    if 'logged_in' not in session:
        abort(403)
    return 'Welcome to admin page.'

? ??通過判斷logged_in是否在session中,我們可以實現:如果用戶已經認證,會返回一行提示文字,否則會返回403錯誤響應.

? ??登出用戶的logout視圖也非常簡單,登出賬戶對應的實際操作其實就是把代表用戶認證的logged_in cookie刪除,這通過session對象的pop方法實現,代碼如下所示。

登出用戶

from flask import session

@app.route('/logout')
def logout():
    if 'logged_in' in session:
        session.pop('logged_in')
    return redirect(url_for('hello'))

? ??現在訪問http://localhost:5000/logout 則會登出用戶,重定向后的/hello頁面的認證狀態信息會變為[Not authenticated],如下圖所示。

未認證的主頁

未認證的主頁

? ??默認情況下,session cookie會在用戶關閉瀏覽器時刪除。通過將session.permanent屬性設為True可以將session的有效期延長為Flask.permanent_session_lifetime屬性值對應的datetime.timedelta對象,也可通過配置變量PERMANENT_SESSION_LIFETIME設置,默認為31天。

? ??盡管session對象會對Cookie進行簽名并加密,但這種方式僅能夠確保session的內容不會被篡改,加密后的數據借助工具仍然可以輕易讀取(即使不知道密鑰)。因此,絕對不能在session中存儲敏感信息,比如用戶密碼。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容