第二章 Flask與HTTP(上)

? ??在第1章猜憎,我們已經(jīng)了解了Flask的基本知識,如果想要進一步開發(fā)更復雜的Flask應(yīng)用,我們就得了解Flask與HTTP協(xié)議的交互方式本缠。HTTP(Hypertext Transfer Protocol,超文本傳輸協(xié)議)定義了服務(wù)器和客戶端之間信息交流的格式和傳遞方式入问,它是萬維網(wǎng)(World Wide Web)中數(shù)據(jù)交換的基礎(chǔ)丹锹。

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

? ??HTTP的詳細定義在RFC 7231~7235中可以看到衬鱼。RFC(Request For Comment业筏,請求評議)是一系列關(guān)于互聯(lián)網(wǎng)標準和信息的文件,可以將其理解為互聯(lián)網(wǎng)(Internet)的設(shè)計文檔鸟赫。完整的RFC列表可以在這里看到:https://tools.ietf.org/rfc/ 蒜胖。

2.1 請求響應(yīng)循環(huán)

1.jpg

? ??有一個類似我們第1章編寫的程序運行著消别。它負責接收用戶的請求,并把對應(yīng)的內(nèi)容返回給客戶端翠勉,顯示在用戶的瀏覽器上妖啥。事實上,每一個Web應(yīng)用都包含這種處理模式对碌,即“請求-響應(yīng)循環(huán)(Request-Response Cycle)”:客戶端發(fā)出請求荆虱,服務(wù)器端處理請求并返回響應(yīng)。

image.png

? ??當用戶訪問一個URL朽们,瀏覽器便生成對應(yīng)的HTTP請求怀读,經(jīng)由互聯(lián)網(wǎng)發(fā)送到對應(yīng)的Web服務(wù)器。Web服務(wù)器接收請求骑脱,通過WSGI將HTTP格式的請求數(shù)據(jù)轉(zhuǎn)換成我們的Flask程序能夠使用的Python數(shù)據(jù)菜枷。在程序中,F(xiàn)lask根據(jù)請求的URL執(zhí)行對應(yīng)的視圖函數(shù)叁丧,獲取返回值生成響應(yīng)啤誊。響應(yīng)依次經(jīng)過WSGI轉(zhuǎn)換生成HTTP響應(yīng),再經(jīng)由Web服務(wù)器傳遞拥娄,最終被發(fā)出請求的客戶端接收蚊锹。瀏覽器渲染響應(yīng)中包含的HTML和CSS代碼,并執(zhí)行JavaScript代碼稚瘾,最終把解析后的頁面呈現(xiàn)在用戶瀏覽器的窗口中牡昆。

2.2 HTTP請求

? ??URL是一個請求的起源。不論服務(wù)器在何地運行摊欠,當我們輸入指向服務(wù)器所在的地址的URL丢烘,都會向服務(wù)器發(fā)送一個HTTP請求趣些。一個標準的URL由很多部分組成导披,以下面這個URL為例:

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

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

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

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

2.2.1 請求報文

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

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

3.jpg

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

4.jpg

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

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

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

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

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 時碟贾,開發(fā)服務(wù)器會在命令行中輸出一條記錄日志,其中包含請求的主要信息:

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

2.2.2 Request對象

? ??假設(shè)請求的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類是字典的子類轨域,它主要實現(xiàn)了同一個鍵對應(yīng)多個值的情況袱耽。比如一個文件上傳字段可能會接收多個文件。這時就可以通過getlist()方法來獲取文件對象列表干发。而ImmutableMultiDict類繼承了MutliDict類朱巨,但其值不可更改。更多內(nèi)容可訪問Werkzeug相關(guān)數(shù)據(jù)結(jié)構(gòu)章節(jié)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')    # 獲取查詢參數(shù)name的值
    return '<h1>Hello, %s!<h1>' % name            

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

? ??輸出:Hello, Grey!

? ??上面的示例代碼包含安全漏洞冀续,在現(xiàn)實中我們要避免直接將用戶傳入的數(shù)據(jù)直接作為響應(yīng)返回,在本章的末尾我們將介紹這個安全漏洞的具體細節(jié)和防范措施必峰。

? ??需要注意的是洪唐,和普通的字典類型不同,當我們從request對象的類型為MutliDict或ImmutableMultiDict的屬性(比如files自点、form桐罕、args)中直接使用鍵作為索引獲取數(shù)據(jù)時(比如request.args['name'])脉让,如果沒有對應(yīng)的鍵桂敛,那么會返回HTTP 400錯誤響應(yīng)(Bad Request,表示請求無效)溅潜,而不是拋出KeyError異常术唬,如圖所示。為了避免這個錯誤滚澜,我們應(yīng)該使用get()方法獲取數(shù)據(jù)粗仓,如果沒有對應(yīng)的值則返回None;get()方法的第二個參數(shù)可以設(shè)置默認值设捐,比如requset.args.get('name'借浊,'Human')。

6.jpg

如果開啟了調(diào)試模式萝招,那么拋出BadRequestKeyError異常并顯示對應(yīng)的錯誤棧信息蚂斤,而不是常規(guī)的404錯誤

2.2.3 在flask中處理請求

? ??URL是指網(wǎng)絡(luò)上資源的地址。在Flask中槐沼,我們需要讓請求的URL匹配對應(yīng)的視圖函數(shù)曙蒸,視圖函數(shù)返回值就是URL對應(yīng)的資源捌治。

1.路由匹配

? ??為了便于將請求分發(fā)到對應(yīng)的函數(shù),程序?qū)嵗鎯α艘粋€路由表(app.url_map)纽窟,其中定義了URL規(guī)則和視圖函數(shù)的映射關(guān)系肖油,當請求發(fā)來后,F(xiàn)lask會根據(jù)請求報文中的URL(path部分)來嘗試與這個表中的所有URL規(guī)則進行匹配,調(diào)用匹配成功的視圖函數(shù).如果沒有匹配的URL規(guī)則,說明程序中沒有處理這個URL的視圖函數(shù),flask]會自動返回404錯誤響應(yīng)(Not Found表示資源未找到),你可以嘗試在瀏覽器中訪問http://localhost:5000/nothing 臂港,因為我們的程序中沒有視圖函數(shù)負責處理這個URL森枪,所以你會得到404響應(yīng)。

? ??當請求的URL與某個視圖函數(shù)的URL規(guī)則匹配成功時趋艘,對應(yīng)的視圖函數(shù)就會調(diào)用疲恢。使用flask routes命令可以查看程序中定義的所有路由,這個列表由app.url_map解析得到:

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


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

? ??在輸出的文本中瓷胧,我們看到每個路由對應(yīng)斷點(Endpoint)显拳、HTTP方法(Methods)和URL規(guī)則(Rule),其中static是flask添加的特殊路由搓萧,用來訪問靜態(tài)文件

2.設(shè)置監(jiān)聽的HTTP方法

? ??通過flask routes命令打印出的路由列表可以看到杂数,每一個路由除了包含URL規(guī)則外,還設(shè)置了監(jiān)聽的HTTP方法瘸洛。GET是最常用的HTTP方法揍移,所以視圖函數(shù)默認監(jiān)聽的方法類型就是GET,HEAD反肋、OPTIONS方法的請求由Flask處理那伐,而像DELETE、PUT等方法一般不會在程序中實現(xiàn)石蔗,在后面我們構(gòu)建Web API時才會用到這些方法罕邀。

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

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

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


image.png

? ??通過定義方法列表敬肚,我們可以為同一個URL規(guī)則定義多個視圖函數(shù),分別處理不同HTTP方法的請求束析。

3.URL處理

? ??從前面的路由列表可以看到艳馒,除了/hello,這個程序還包含許多URL規(guī)則,比如和go_back端點對應(yīng)的/goback/<int:year>』福現(xiàn)在請嘗試訪問http://localhost:5000/goback/34 鹰溜,在URL中加入一個數(shù)字作為時光倒流的年數(shù)虽填,你會發(fā)現(xiàn)加載后的頁面中有通過傳入的年數(shù)計算出的年份:“Welcome to 1984!”曹动。仔細觀察一下斋日,你會發(fā)現(xiàn)URL規(guī)則中的變量部分有一些特別,<int:year>表示為year變量添加了一個int轉(zhuǎn)換器墓陈,F(xiàn)lask在解析這個URL變量時會將其轉(zhuǎn)換為整型恶守。URL中的變量部分默認類型為字符串,但Flask提供了一些轉(zhuǎn)換器可以在URL規(guī)則里使用贡必,如下表所示:

??Flask內(nèi)置的URL變量轉(zhuǎn)換器

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

? ??轉(zhuǎn)換器通過特定的規(guī)則指定兔港,即"<轉(zhuǎn)換器:變量名>"。"<int: year>"把year的值轉(zhuǎn)換為整數(shù)仔拟,因此我們可以在視圖函數(shù)中直接對year變量進行科學計算:

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

? ??默認的行為不僅僅是類型轉(zhuǎn)換衫樊,還包括URL匹配。在這個例子中利花,如果不使用轉(zhuǎn)換器科侈,默認year變量會被轉(zhuǎn)換成字符串,為了能夠在Python中計算天數(shù)炒事,我們需要使用int()函數(shù)將year變量轉(zhuǎn)換成整型臀栈。但是如果用戶輸入的是英文字母,就會出現(xiàn)轉(zhuǎn)換錯誤挠乳,拋出ValueError異常权薯,我們還需要手動驗證;使用了轉(zhuǎn)換器后睡扬,如果URL中傳入的變量不是數(shù)字盟蚣,那么會直接返回404錯誤響應(yīng)。比如威蕉,你可以嘗試訪問http://localhost:5000/goback/tang 刁俭。

? ??在用法上唯一特別的是any轉(zhuǎn)換器橄仍,你需要在轉(zhuǎn)換器后添加括號來給出可選值韧涨,即"<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轉(zhuǎn)換器中設(shè)置的可選值以外的任意字符侮繁,均會獲得404錯誤響應(yīng)虑粥。

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

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

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

2.2.4 請求鉤子

? ??有時我們需要對請求進行預(yù)處理(preprocessing)和后處理(postprocessing)娩贷,這時可以使用Flask提供的一些請求鉤子(Hook),他們可以用來注冊在請求處理的不同階段執(zhí)行的處理函數(shù)(或稱為回調(diào)函數(shù)锁孟,即Callback)彬祖。這些請求鉤子使用裝飾器實現(xiàn)茁瘦,通過程序?qū)嵗齛pp調(diào)用,用法很簡單:以before_request鉤子(請求之前)為例储笑,當你對一個函數(shù)附加了app.before_request裝飾器后甜熔,就會將這個函數(shù)注冊為before_request處理函數(shù),每次執(zhí)行請求前都會觸發(fā)所有before_request處理函數(shù)突倍。Flask默認實現(xiàn)的五種請求鉤子如下表所示:

請求鉤子

鉤子 說明
before_first_request 注冊一個函數(shù)腔稀,在處理第一個請求運行
before_request 注冊一個函數(shù),在處理每個請求前運行
after_request 注冊一個函數(shù)羽历,如果沒有未處理的異常拋出焊虏,會在每個請求結(jié)束后運行
teardown_request 注冊一個函數(shù),即使有未處理的異常拋出秕磷,會在每個請求結(jié)束后運行诵闭。如果發(fā)生異常,會傳入異常對象作為參數(shù)到注冊的函數(shù)中
after_this_request 在視圖函數(shù)內(nèi)注冊一個函數(shù)澎嚣,會在這個請求結(jié)束后運行

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

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

? ??假如我們創(chuàng)建了三個視圖函數(shù)A润歉、B、C颈抚,其中視圖C使用了after_this_reques鉤子踩衩,那么當請求A進入后,整個請求處理周期的請求處理函數(shù)調(diào)用流程如下所示贩汉。

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

·before_first_request:在玩具程序中驱富,運行程序前我們需要進行一些程序的初始化操作,比如創(chuàng)建數(shù)據(jù)庫表匹舞,添加管理員用戶褐鸥。這些工作可以放到使用before_first_request裝飾器注冊的函數(shù)中。

·before_request:比如網(wǎng)站上要記錄用戶最后在線的時間赐稽,可以通過用戶最后發(fā)送的請求時間來實現(xiàn)叫榕。為了避免在每個視圖函數(shù)都添加更新在線時間的代碼,我們可以僅在使用before_request鉤子注冊的函數(shù)中調(diào)用這段代碼姊舵。

·after_request:我們經(jīng)常在視圖函數(shù)中進行數(shù)據(jù)庫操作晰绎,比如更新、插入等括丁,之后需要將更改提交到數(shù)據(jù)庫中荞下。提交更改的代碼就可以放到after_request鉤子注冊的函數(shù)中。

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

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

注意

? ??after_request鉤子和after_this_request鉤子必須接收一個響應(yīng)類對象作為參數(shù)掸鹅,并且返回同一個或更新后的響應(yīng)對象塞帐。

2.3 HTTP響應(yīng)

? ??在Flask程序中,客戶端發(fā)出的請求觸發(fā)響應(yīng)的視圖函數(shù)巍沙,獲取返回值會作為響應(yīng)的主體最后生成完整的響應(yīng)葵姥,即響應(yīng)報文

2.3.1 響應(yīng)報文

? ??響應(yīng)報文主要由協(xié)議版本、狀態(tài)碼(status code)句携、原因短語(reason phrase)榔幸、響應(yīng)首部和響應(yīng)主體組成。以發(fā)向localhost:5000/hello的請求為例矮嫉,服務(wù)器生成的響應(yīng)報文示意如圖所示

響應(yīng)報文

響應(yīng)報文

? ??響應(yīng)報文的首部包含一些關(guān)于響應(yīng)和服務(wù)器的信息削咆,這些內(nèi)容由Flask生成,而我們在視圖函數(shù)中返回的內(nèi)容即為響應(yīng)報文中的主體內(nèi)容蠢笋。瀏覽器接收到響應(yīng)后拨齐,會把返回的響應(yīng)主體解析并顯示在瀏覽器窗口上。

? ??HTTP狀態(tài)碼用來表示請求處理的結(jié)果昨寞,下表是常見的幾種狀態(tài)碼和相應(yīng)的原因短語瞻惋。

常見的HTTP狀態(tài)碼

常見的HTTP狀態(tài)碼

當關(guān)閉調(diào)試模式,即FLASK_ENV使用默認值production,如果程序出錯,Flask會自動返回500錯誤響應(yīng),而調(diào)試模式下則會顯示調(diào)試信息和錯誤堆棧

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

2.3.2在Flask中生成響應(yīng)

? ??響應(yīng)在Flask中使用Response對象表示,響應(yīng)報文中的大部分內(nèi)容由服務(wù)器處理援岩,大多數(shù)情況下歼狼,我們只負責返回主體內(nèi)容。

? ??根據(jù)我們在上一節(jié)介紹的內(nèi)容享怀,F(xiàn)lask會先判斷是否可以找到與請求URL相匹配的路由羽峰,如果沒有則返回404響應(yīng)。如果找到添瓷,則調(diào)用對應(yīng)的視圖函數(shù)梅屉,視圖函數(shù)的返回值構(gòu)成了響應(yīng)報文的主體內(nèi)容,正確返回時狀態(tài)碼默認為200仰坦。Flask會調(diào)用make_response()方法將視圖函數(shù)返回值轉(zhuǎn)換為響應(yīng)對象履植。

? ??完整地說计雌,視圖函數(shù)可以返回最多由三個元素組成的元組:響應(yīng)主體悄晃、狀態(tài)碼、首部字段。其中首部字段可以為字典妈橄,或是兩元素元組組成的列表庶近。

? ??比如,普通的響應(yīng)可以只包含主體內(nèi)容:

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

? ??默認的狀態(tài)碼為200眷蚓,下面指定了不同的狀態(tài)碼:

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

? ??有時你會想附加或修改某個首部字段鼻种。比如,要生成狀態(tài)碼為3XX的重定向響應(yīng)沙热,需要將首部中的Location字段設(shè)置為重定向的目標URL:

? ```

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

? ??現(xiàn)在訪問http://localhost:5000/hello 叉钥,會重定向到http://www.example.com 。在多數(shù)情況下篙贸,除了響應(yīng)主體投队,其他部分我們通常只需要使用默認值即可。

1.重定向

? ??如果你訪問http://localhost:5000/hi 爵川,你會發(fā)現(xiàn)頁面加載后地址欄中的URL變?yōu)榱?a href="http://localhost:5000/hello" target="_blank" rel="nofollow">http://localhost:5000/hello 敷鸦。這種行為被稱為重定向(Redirect),你可以理解為網(wǎng)頁跳轉(zhuǎn)寝贡。在上一節(jié)的示例中扒披,狀態(tài)碼為302的重定向響應(yīng)的主體為空,首部中需要將Location字段設(shè)為重定向的目標URL圃泡,瀏覽器接收到重定向響應(yīng)后會向Location字段中的目標URL發(fā)起新的GET請求碟案,整個流程如圖所示。

重定向流程示意圖

重定向流程示意圖

? ??在Web程序中颇蜡,我們經(jīng)常需要進行重定向蟆淀。比如,當某個用戶在沒有經(jīng)過認證的情況下訪問需要登錄后才能訪問的資源澡匪,程序通常會重定向到登錄頁面熔任。

? ??對于重定向這一類特殊響應(yīng),F(xiàn)lask提供了一些輔助函數(shù)唁情。除了像前面那樣手動生成302響應(yīng)疑苔,我們可以使用Flask提供的redirect()函數(shù)來生成重定向響應(yīng),重定向的目標URL作為第一個參數(shù)甸鸟。前面的例子可以簡化為:

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

? ??使用redirect()函數(shù)時惦费,默認的狀態(tài)碼為302,即臨時重定向抢韭。如果你想修改狀態(tài)碼薪贫,可以在redirect()函數(shù)中作為第二個參數(shù)或使用code關(guān)鍵字傳入。

? ??如果要在程序內(nèi)重定向到其他視圖刻恭,那么只需在redirect()函數(shù)中使用url_for()函數(shù)生成目標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.錯誤響應(yīng)

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

418錯誤響應(yīng)

418錯誤響應(yīng)

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

? ??大多數(shù)情況下梁棠,F(xiàn)lask會自動處理常見的錯誤響應(yīng)置森。HTTP錯誤對應(yīng)的異常類在Werkzeug的werkzeug.exceptions模塊中定義,拋出這些異常即可返回對應(yīng)的錯誤響應(yīng)符糊。如果你想手動返回錯誤響應(yīng)暇藏,更方便的方法是使用Flask提供的abort()函數(shù)。

? ??在abort()函數(shù)中傳入狀態(tài)碼即可返回對應(yīng)的錯誤響應(yīng)濒蒋,下面代碼中的視圖函數(shù)返回404錯誤響應(yīng)盐碱。

返回404錯誤響應(yīng)

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

abort()函數(shù)前不需要使用return語句,但一旦abort()函數(shù)被調(diào)用沪伙,abort()函數(shù)之后的代碼將不會被執(zhí)行瓮顽。

? ??雖然我們有必要返回正確的狀態(tài)碼,但這并不是必須的围橡。比如暖混,當某個用戶沒有權(quán)限訪問某個資源時,返回404錯誤要比403錯誤更加友好

2.3.3 響應(yīng)格式

? ??在HTTP響應(yīng)中翁授,數(shù)據(jù)可以通過多種格式傳輸拣播。大多數(shù)情況下,我們會使用HTML格式收擦,這也是Flask中的默認設(shè)置贮配。在特定的情況下,我們也會使用其他格式塞赂。不同的響應(yīng)數(shù)據(jù)格式需要設(shè)置不同的MIME類型泪勒,MIME類型在首部的Content-Type字段中定義,以默認的HTML類型為例:

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

? ??MIME類型(又稱為media type或content type)是一種用來標識文件類型的機制宴猾,它與文件擴展名相對應(yīng)圆存,可以讓客戶端區(qū)分不同的內(nèi)容類型,并執(zhí)行不同的操作仇哆。一般的格式為“類型名/子類型名”沦辙,其中的子類型名一般為文件擴展名。比如讹剔,HTML的MIME類型為“text/html”油讯,png圖片的MIME類型為“image/png”详民。完整的標準MIME類型列表可以在這里看到:https://www.iana.org/assignments/media-types/media-types.xhtml

? ??如果你想使用其他MIME類型撞羽,可以通過Flask提供的make_response()方法生成響應(yīng)對象阐斜,傳入響應(yīng)的主體作為參數(shù)衫冻,然后使用響應(yīng)對象的mimetype屬性設(shè)置MIME類型诀紊,比如:

from flask import make_response

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

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

2.3.4 Cookie

? ??HTTP是無狀態(tài)(stateless)協(xié)議碌宴。也就是說,在一次請求響應(yīng)結(jié)束后蒙畴,服務(wù)器不會留下任何關(guān)于對方狀態(tài)的信息。但是對于某些Web程序來說膳凝,客戶端的某些信息又必須被記住,比如用戶的登錄狀態(tài)上煤,這樣才可以根據(jù)用戶的狀態(tài)來返回不同的響應(yīng)。為了解決這類問題著淆,就有了Cookie技術(shù)劫狠。Cookie技術(shù)通過在請求和響應(yīng)報文中添加Cookie數(shù)據(jù)來保存客戶端的狀態(tài)信息。

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

? ??在Flask中孕惜,如果想要在響應(yīng)中添加一個cookie,最方便的方法是使用Response類提供的set_cookie()方法晨炕。要使用這個方法衫画,我們需要先使用make_response()方法手動生成一個響應(yīng)對象,傳入響應(yīng)主體作為參數(shù)瓮栗。這個響應(yīng)對象默認實例化內(nèi)置的Response類削罩。下表是內(nèi)置的Response類常用的屬性和方法瞄勾。

Response類常用的屬性和方法

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

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

set_cookie()方法的參數(shù)

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

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

設(shè)置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()函數(shù)中,我們傳入的是使用redirect()函數(shù)生成的重定向響應(yīng)弥激。set_cookie視圖會在生成的響應(yīng)報文首部中創(chuàng)建一個Set-Cookie字段进陡,即“Set-Cookie:name=Grey;Path=/”微服。

? ??現(xiàn)在我們查看瀏覽器中的Cookie趾疚,就會看到多了一塊名為name的cookie以蕴,其值為我們設(shè)置的“Grey”赡磅,如下圖所示焚廊。因為過期時間使用默認值,所以會在瀏覽會話結(jié)束時(關(guān)閉瀏覽器)過期搞疗。

在瀏覽器中查看cookie

image.png

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

Cookie設(shè)置示意圖

9.jpg

? ??在Flask中,Cookie可以通過請求對象的cookie屬性讀取,在修改后的hello視圖中,如果沒有從查詢參數(shù)中獲取到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

? ??這時服務(wù)器就可以根據(jù)Cookie的內(nèi)容來獲得客戶端的狀態(tài)信息豌汇,并根據(jù)狀態(tài)返回不同的響應(yīng)宛徊。如果你訪問http://localhost:5000/set/Grey 闸天,那么就會將名為name的cookie設(shè)為Grey苞氮,重定向到/hello后库物,你會發(fā)現(xiàn)返回的內(nèi)容變成了“Hello戚揭,Grey民晒!”镀虐。如果你再次通過訪問http://localhost:5000/set/ 修改name cookie的值,那么重定向后的頁面返回的內(nèi)容也會隨之改變绽慈。

2.3.5 session:安全的Cookie

? ??當我們使用瀏覽器登錄某個社交網(wǎng)站時,會在登錄表單中填寫用戶名和密碼钝凶,單擊登錄按鈕后耕陷,這會向服務(wù)器發(fā)送一個包含認證數(shù)據(jù)的請求哟沫。服務(wù)器接收請求后會查找對應(yīng)的賬戶,然后驗證密碼是否匹配隆敢,如果匹配拂蝎,就在返回的響應(yīng)中設(shè)置一個cookie封救,比如誉结,“l(fā)ogin_user:greyli”惩坑。

? ??響應(yīng)被瀏覽器接收后,cookie會被保存在瀏覽器中蔓钟。當用戶再次向這個服務(wù)器發(fā)送請求時滥沫,根據(jù)請求附帶的Cookie字段中的內(nèi)容,服務(wù)器上的程序就可以判斷用戶的認證狀態(tài)缀辩,并識別出用戶。

? ??但是這會帶來一個問題杯瞻,在瀏覽器中手動添加和修改Cookie是很容易的事魁莉,僅僅通過瀏覽器插件就可以實現(xiàn)旗唁。所以讶请,如果直接把認證信息以明文的方式存儲在Cookie里论巍,那么惡意用戶就可以通過偽造cookie的內(nèi)容來獲得對網(wǎng)站的權(quán)限,冒用別人的賬戶鞋怀。為了避免這個問題,我們需要對敏感的Cookie內(nèi)容進行加密残腌。方便的是揍瑟,F(xiàn)lask提供了session對象用來將Cookie數(shù)據(jù)加密儲存。

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

1.設(shè)置程序密鑰

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

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

app.secret_key = 'secret string'

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

? SECRET_KEY = secret string

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

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

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

這里的密鑰只是示例。在生產(chǎn)環(huán)境中畏腕,為了安全考慮,你必須使用隨機生成的密鑰

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,將它的值設(shè)為True,表示用戶已認證速蕊。

? ??當我們使用session對象添加cookie時竿奏,數(shù)據(jù)會使用程序的密鑰對其進行簽名,加密后的數(shù)據(jù)存儲在一塊名為session的cookie里聚请,如下圖所示盖文。

? ??你可以在下圖方框內(nèi)的Content部分看到對應(yīng)的加密處理后生成的session值。使用session對象存儲的Cookie,用戶可以看到其加密后的值蝉仇,但無法修改它鞭呕。因為session中的內(nèi)容使用密鑰進行簽名枷恕,一旦數(shù)據(jù)被修改,簽名的值也會變化瞧掺。這樣在讀取時壹蔓,就會驗證失敗行疏,對應(yīng)的session值也會隨之失效。所以销部,除非用戶知道密鑰狰右,否則無法對session cookie的值進行修改情臭。

image.png

? ??當支持用戶登錄后,我們就可以根據(jù)用戶的認證狀態(tài)分別顯示不同的內(nèi)容。在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
        # 根據(jù)用戶認證狀態(tài)返回不同的內(nèi)容
    if 'logged_in' in session:
        response += '[Authenticated]'
    else:
        response += '[Not Authenticated]'
    return response

? ??session中的數(shù)據(jù)可以像字典一樣通過鍵讀取,或是使用get()方法谦秧。這里我們只是判斷session中是否包含logged_in鍵竟纳,如果有則表示用戶已經(jīng)登錄撵溃。通過判斷用戶的認證狀態(tài),我們在返回的響應(yīng)中添加一行表示認證狀態(tài)的信息:如果用戶已經(jīng)登錄锥累,顯示[Authenticated]缘挑;否則顯示[Not authenticated]。

??如果你訪問http://localhost:5000/login 桶略,就會登入當前用戶语淘,重定向到http://localhost:5000/hello 后你會發(fā)現(xiàn)加載后的頁面顯示一行“[Authenticated]”,表示當前用戶已經(jīng)通過認證删性,如下圖所示亏娜。

已認證主頁

已認證主頁

? ??程序中的某些資源僅提供給登入的用戶,比如管理后臺蹬挺,這時我們就可以通過判斷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.'

? ??通過判斷l(xiāng)ogged_in是否在session中,我們可以實現(xiàn):如果用戶已經(jīng)認證,會返回一行提示文字,否則會返回403錯誤響應(yīng).

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

登出用戶

from flask import session

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

? ??現(xiàn)在訪問http://localhost:5000/logout 則會登出用戶,重定向后的/hello頁面的認證狀態(tài)信息會變?yōu)閇Not authenticated]榕茧,如下圖所示垃沦。

未認證的主頁

未認證的主頁

? ??默認情況下,session cookie會在用戶關(guān)閉瀏覽器時刪除用押。通過將session.permanent屬性設(shè)為True可以將session的有效期延長為Flask.permanent_session_lifetime屬性值對應(yīng)的datetime.timedelta對象肢簿,也可通過配置變量PERMANENT_SESSION_LIFETIME設(shè)置,默認為31天蜻拨。

? ??盡管session對象會對Cookie進行簽名并加密池充,但這種方式僅能夠確保session的內(nèi)容不會被篡改,加密后的數(shù)據(jù)借助工具仍然可以輕易讀榷兴稀(即使不知道密鑰)收夸。因此,絕對不能在session中存儲敏感信息血崭,比如用戶密碼卧惜。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市夹纫,隨后出現(xiàn)的幾起案子咽瓷,更是在濱河造成了極大的恐慌,老刑警劉巖舰讹,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忱详,死亡現(xiàn)場離奇詭異,居然都是意外死亡跺涤,警方通過查閱死者的電腦和手機匈睁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桶错,“玉大人航唆,你說我怎么就攤上這事≡旱螅” “怎么了糯钙?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長退腥。 經(jīng)常有香客問我任岸,道長,這世上最難降的妖魔是什么狡刘? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任享潜,我火速辦了婚禮,結(jié)果婚禮上嗅蔬,老公的妹妹穿的比我還像新娘剑按。我一直安慰自己,他們只是感情好澜术,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布艺蝴。 她就那樣靜靜地躺著,像睡著了一般鸟废。 火紅的嫁衣襯著肌膚如雪猜敢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天盒延,我揣著相機與錄音缩擂,去河邊找鬼。 笑死兰英,一個胖子當著我的面吹牛撇叁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畦贸,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陨闹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了薄坏?” 一聲冷哼從身側(cè)響起趋厉,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胶坠,沒想到半個月后君账,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡沈善,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年乡数,在試婚紗的時候發(fā)現(xiàn)自己被綠了椭蹄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡净赴,死狀恐怖绳矩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情玖翅,我是刑警寧澤翼馆,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站金度,受9級特大地震影響应媚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猜极,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一中姜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧魔吐,春花似錦扎筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辞色,卻和暖如春骨宠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背相满。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工层亿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人立美。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓匿又,卻偏偏與公主長得像,于是被迫代替她去往敵國和親建蹄。 傳聞我的和親對象是個殘疾皇子碌更,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容