? ??在第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章編寫的程序運行著消别。它負責接收用戶的請求,并把對應(yīng)的內(nèi)容返回給客戶端翠勉,顯示在用戶的瀏覽器上妖啥。事實上,每一個Web應(yīng)用都包含這種處理模式对碌,即“請求-響應(yīng)循環(huán)(Request-Response Cycle)”:客戶端發(fā)出請求荆虱,服務(wù)器端處理請求并返回響應(yīng)。
? ??當用戶訪問一個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)生的請求報文示意表如下所示:
? ??如果你想看真實的HTTP報文骏全,可以在瀏覽器中向任意一個有效的URL發(fā)起請求,然后在瀏覽器的開發(fā)者工具(F12)里的Network標簽中看到URL對應(yīng)資源加載的所有請求列表尼斧,單擊任一個請求條目即可看到報文信息姜贡,如下所示:
? ??報文由報文首部和報文主體組成,兩者由空行分隔,請求報文的主體一般為空.如果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對象常用的屬性和方法:
? ??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')。
如果開啟了調(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耘纱, 表示請求方法不允許),如圖所示:
? ??通過定義方法列表敬肚,我們可以為同一個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)報文的首部包含一些關(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)碼
當關(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)由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
? ??當瀏覽器保存了服務(wù)端設(shè)置的cookie后,瀏覽器再次發(fā)送到該服務(wù)器的請求會自動攜帶設(shè)置的Cookie信息,Cookie的內(nèi)容存儲在請求首部的Cookie字段中,整個交互過程由上至下如下圖所示:
Cookie設(shè)置示意圖
? ??在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的值進行修改情臭。
? ??當支持用戶登錄后,我們就可以根據(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中存儲敏感信息血崭,比如用戶密碼卧惜。