Python全棧(七)Flask框架之12.Cookie、Session、上下文和鉤子函數

君子之交淡如水,小人之交甘若醴。
君子之間的交情,並不因利益驅使而怎樣,小人之間的交往,卻多因利益驅使,利益過後,人與人如過眼雲煙。君子之間,不因無利益不互相關心,小人之間,卻多因利益關係而互相勾結。

一、Cookie的使用

1.Cookie的基本概念

在網站中,http請求是無狀態的,也就是說即使第一次和服務器連接後並且登錄成功後,第二次請求服務器依然不能知道當前請求是哪個用戶。
cookie的出現就是爲了解決這個問題,類型爲小型文本文件,是某些網站爲了辨別用戶身份,進行Session跟蹤而儲存在用戶本地終端上的數據(通常經過加密),由用戶客戶端計算機暫時或永久保存的信息。
第一次登錄後服務器返回一些數據(cookie)給瀏覽器,然後瀏覽器保存在本地,當該用戶發送第二次請求的時候,就會自動將上次請求存儲的cookie數據自動傳送給服務器,服務器通過瀏覽器攜帶的數據來判斷用戶。
cookie存儲的數據量有限,不同的瀏覽器有不同的存儲大小,但一般不超過4KB,因此使用cookie只能存儲一些小量的數據。

2.Flask中使用Cookie

在Flask中操作cookie,是通過Response對象來操作,可以在response返回之前,通過response.set_cookie()方法來設置,這個方法有以下幾個參數:

  • key
    cookie的鍵
  • value
    cookie的鍵對應的值。
  • max_age
    cookie的過期時間,如果不設置,則瀏覽器關閉後就會自動過期。
  • expires
    過期時間,時間戳的形式(1970到現在的時間)。
  • domain
    該cookie在哪個域名中有效,一般設置子域名,比如cms.example.com。
  • path
    該cookie在哪個路徑下有效,即當前主域名。

cookie簡單測試:
flask_app.py如下:

from flask import Flask, Response

app = Flask(__name__)
app.config['TEMPLATE_AUTO_RELOAD'] = True


@app.route('/')
def index():
    return '首頁'


@app.route('/cookietest/')
def cookietest():
    res = Response('Hello World!!')
    res.set_cookie('username', value='corley', max_age=3)
    return res


if __name__ == '__main__':
    app.run(debug=True)

顯示:

flask cookie simple use

二、Flask-session的介紹和使用

1.Session的基本概念

session和cookie的作用有點類似,都是爲了存儲用戶相關的信息;
session可以類比字典,每個鍵對應着指定的值。
不同的是,cookie存儲在本地瀏覽器,session是一個思路、一個概念、一個服務器存儲授權信息的解決方案,不同的服務器、不同的框架、不同的語言有不同的實現。雖然實現不一樣,但是目的都是爲了在服務器更方便地存儲數據。
session是爲了解決cookie存儲數據不安全的問題。

在現實中,cookie和session是可以結合使用的,有兩種方式:

  • 存儲在服務端
    通過cookie存儲一個session_id,具體的數據則是保存在session中。如果用戶已經登錄,則服務器會在cookie中保存一個session_id,下次再次請求的時候,會攜帶該session_id,服務器根據session_id在session庫中獲取用戶的session數據,就能判斷該用戶到底是誰,以及之前保存的一些狀態信息,這稱爲server side session
    數據存儲在服務器會更加安全,不容易被竊取,包括Django在內的很多框架都是採用的這種形式。
    但存儲在服務器也有一定的弊端,就是會佔用一定的服務器資源,但現在服務器存儲量一般較大,存儲session信息是不成問題的。
  • 存儲在客戶端
    將session數據加密,然後存儲在cookie中,這種稱爲client side session
    flask採用的就是這種方式,但是也可以替換成其他形式。

2.Flask中使用Session

Flask中中使用session需要先通過from flask import session語句導入,再添加key和value。
Flask中的session機制是將session信息加密,然後存儲在cookie中,因此需要通過配置信息app.config['SECRET_KEY']設置加密密鑰。

session的基本使用測試如下:

from flask import Flask, session
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)   # os.urandom()生成指定字節數的隨機字符串


@app.route('/')
def index():
    session['username'] = 'Corley'
    session['user_id'] = 1
    return '這是首頁'


if __name__ == '__main__':
    app.run(debug=True)

顯示:
flask session use
顯然,上傳了session的一些信息;
其中,os.urandom()用於生成指定字節數的隨機字符串,用於表示密鑰;
同時可以看到,到期時間是瀏覽器會話結束時。

可以指定會話到期時間,如下:

from flask import Flask, session
from datetime import timedelta
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
# 自定義過期時間
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)


@app.route('/')
def index():
    session['username'] = 'Corley'
    session['user_id'] = 1
    # 設置持久化
    session.permanent = True
    return '這是首頁'


if __name__ == '__main__':
    app.run(debug=True)

顯示:
flask session use set time
顯然,此時session的過期時間爲2個小時。
如果想自定義過期時間,需要進行兩方面配置:
(1)設置持久化

session.permanent = True

設置了持久化後,默認的過期時間爲1個月,如果想要自定義過期時間,還需要進行第二步配置。
(2)自定義過期時間

app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)

還可以獲取session值,測試如下:

from flask import Flask, session
from datetime import timedelta
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)


@app.route('/')
def index():
    username = session.get('username')
    return '這是首頁' + str(username)


@app.route('/login/')
def login():
    session['username'] = 'Corley'
    session['user_id'] = 1
    session.permanent = True
    return '登錄頁面'


if __name__ == '__main__':
    app.run(debug=True)

顯示:
flask session use get value
顯然,獲取到了session的信息。

可以刪除session,如下:

from flask import Flask, session
from datetime import timedelta
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)


@app.route('/')
def index():
    username = session.get('username')
    return '這是首頁' + str(username)


@app.route('/login/')
def login():
    session['username'] = 'Corley'
    session['user_id'] = 1
    session.permanent = True
    return '登錄頁面'

@app.route('/logout/')
def logout():
    # 刪除session
    session.clear()
    return '退出登錄'+ str(session.get('username'))

if __name__ == '__main__':
    app.run(debug=True)

顯示:
flask session use delete
顯然,在訪問http://127.0.0.1:5000/logout/後,session已經清空;
除了session.clear(),還可以指定key刪除session,如刪除username可以用session.pop('username')

三、Flask上下文

Flask項目中上下文包括兩類:

  • 應用上下文
    用於記錄和應用相關的數據,包括current_app和g。
  • 請求上下文
    用於記錄和請求相關的數據,包括request和session。

請求上下文request和應用上下文current_app都是全局變量,所有請求都是共享的。
Flask中使用特殊的機制來保證每次請求的數據都是隔離的,即用戶A請求所產生的數據不會影響到用戶B的請求,所以直接導入request對象也不會被一些髒數據影響,並且不需要像Django一樣在每個函數中使用request的時候傳入request參數。
4類上下文對象如下:

  • request
    請求上下文對象,一般用來保存一些請求的變量,如method、args、form等。
  • session
    請求上下文對象,一般用來保存一些會話信息。
  • current_app
    應用上下文對象,返回當前的app。
  • g
    應用上下文對象,處理請求時用於臨時存儲的對象。

current_app使用示意如下:

from flask import Flask, current_app

app = Flask(__name__)


@app.route('/')
def index():
    return '這是首頁'


@app.route('/login/')
def login():
    ca = current_app.name
    cfg = current_app.config['DEBUG']
    return '登錄頁面:' + ca + '-' + str(cfg)


if __name__ == '__main__':
    app.run(debug=True)

顯示:
flask context current_app use
顯然,得到了當前所處的app,即初始化Flask對象時傳入的參數__name__,也就是當前Python文件名;
除了獲取當前app名稱,還可以獲取綁定到app中的配置參數。

注意:current_app只能在視圖函數中使用,在視圖函數之外使用會報錯。

一般情況測試:
新建utils.py如下:

'''
工具文件
'''


def log_a(username):
    print("Log A %s" % username)


def log_b(username):
    print("Log B %s" % username)

主程序flask_context.py如下:

from flask import Flask, session
from utils import log_a, log_b
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)


@app.route('/')
def index():
    username = session.get('username')
    log_a(username)
    log_b(username)
    return '這是首頁'


@app.route('/login/')
def login():
    session['username'] = 'Corley'
    session['user_id'] = 1
    return '登錄頁面'


if __name__ == '__main__':
    app.run(debug=True)

運行後,先訪問http://127.0.0.1:5000/,再訪問http://127.0.0.1:5000/login/,再訪問http://127.0.0.1:5000/,控制檯打印如下:

127.0.0.1 - - [13/May/2020 11:57:59] "GET / HTTP/1.1" 200 -
Log A None
Log B None
127.0.0.1 - - [13/May/2020 11:58:14] "GET /login/ HTTP/1.1" 200 -
127.0.0.1 - - [13/May/2020 11:58:23] "GET / HTTP/1.1" 200 -
Log A Corley
Log B Corley

顯然,在訪問登錄頁面之後,控制檯打印出了username信息。

此時使用g對象進行改進:
主程序文件修改如下:

from flask import Flask, current_app, g, session
from utils import log_a, log_b
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)


@app.route('/')
def index():
    g.username = session.get('username')
    log_a()
    log_b()
    return '這是首頁'


@app.route('/login/')
def login():
    session['username'] = 'Corley'
    session['user_id'] = 1
    return '登錄頁面'


if __name__ == '__main__':
    app.run(debug=True)

utils.py修改如下:

'''
工具文件
'''
from flask import g

def log_a():
    print("Log A %s" % g.username)


def log_b():
    print("Log B %s" % g.username)

運行之後,按之前的順序訪問,控制檯打印:

127.0.0.1 - - [13/May/2020 12:04:34] "GET / HTTP/1.1" 200 -
Log A None
Log B None
127.0.0.1 - - [13/May/2020 12:04:42] "GET /login/ HTTP/1.1" 200 -
127.0.0.1 - - [13/May/2020 12:04:52] "GET / HTTP/1.1" 200 -
Log A Corley
Log B Corley

顯然,達到了同樣的效果,並且此時不需要再在log_a()log_b()函數中傳遞參數;
每次刷新頁面後,g對象都會被重置,是臨時存儲的對象;
g對象一般用於解決頻繁傳參的問題。

可以在一個視圖函數中實現調用另一個函數,如下:

from flask import Flask, current_app, g, session
from utils import log_a, log_b
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)


@app.route('/')
def index():
    g.username = session.get('username')
    log_a()
    log_b()
    hello()
    return '這是首頁'


def hello():
    print('Hello %s' % g.username)


@app.route('/login/')
def login():
    session['username'] = 'Corley'
    session['user_id'] = 1
    return '登錄頁面'


if __name__ == '__main__':
    app.run(debug=True)

按之前的順序訪問,控制檯打印如下:

127.0.0.1 - - [13/May/2020 12:13:41] "GET / HTTP/1.1" 200 -
Log A None
Log B None
Hello None
127.0.0.1 - - [13/May/2020 12:13:48] "GET /login/ HTTP/1.1" 200 -
127.0.0.1 - - [13/May/2020 12:13:52] "GET / HTTP/1.1" 200 -
Log A Corley
Log B Corley
Hello Corley

四、常用的鉤子函數

鉤子函數是指在執行函數和目標函數之間掛載的函數,框架開發者給調用方提供一個point即掛載點,至於掛載什麼函數由調用方決定, 從而大大提高了靈活性

1.before_first_request

第一次請求之前執行。
練習如下:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    print('這是首頁')
    return '這是首頁'


@app.before_first_request
def handle_first_request():
    print('在第一次請求之前執行')


if __name__ == '__main__':
    app.run(debug=True)

運行後,訪問http://127.0.0.1:5000/,控制檯打印:

127.0.0.1 - - [13/May/2020 12:39:01] "GET / HTTP/1.1" 200 -
在第一次請求之前執行
這是首頁

2.before_request

在每次請求之前執行,通常可以用這個裝飾器來給視圖函數增加一些變量。

練習如下:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    print('這是首頁')
    return '這是首頁'


@app.before_first_request
def handle_first_request():
    print('在第一次請求之前執行')


@app.before_request
def handle_before():
    print("在每一次請求之前執行")


if __name__ == '__main__':
    app.run(debug=True)

運行後,多次訪問http://127.0.0.1:5000/,控制檯打印:

在第一次請求之前執行
在每一次請求之前執行
這是首頁
127.0.0.1 - - [13/May/2020 12:41:36] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
127.0.0.1 - - [13/May/2020 12:41:38] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
127.0.0.1 - - [13/May/2020 12:41:39] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
127.0.0.1 - - [13/May/2020 12:41:40] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [13/May/2020 12:41:41] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁

3.after_request

在每次請求之後執行。

練習如下:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    print('這是首頁')
    return '這是首頁'


@app.before_first_request
def handle_first_request():
    print('在第一次請求之前執行')


@app.before_request
def handle_before():
    print("在每一次請求之前執行")


@app.after_request
def handle_after(response):
    print("在每一次請求之後執行")
    return response


if __name__ == '__main__':
    app.run(debug=True)

運行後,多次訪問http://127.0.0.1:5000/,控制檯打印:

127.0.0.1 - - [13/May/2020 12:51:05] "GET / HTTP/1.1" 200 -
在第一次請求之前執行
在每一次請求之前執行
這是首頁
在每一次請求之後執行
在每一次請求之前執行
這是首頁
在每一次請求之後執行
127.0.0.1 - - [13/May/2020 12:51:06] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
在每一次請求之後執行
127.0.0.1 - - [13/May/2020 12:51:07] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
在每一次請求之後執行
127.0.0.1 - - [13/May/2020 12:51:08] "GET / HTTP/1.1" 200 -
在每一次請求之前執行
這是首頁
在每一次請求之後執行
127.0.0.1 - - [13/May/2020 12:51:09] "GET / HTTP/1.1" 200 -

4.teardown_appcontext

不管是否有異常,註冊的函數都會在每次請求之後執行。

練習如下:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    1/0
    print('這是首頁')
    return '這是首頁'


@app.before_first_request
def handle_first_request():
    print('在第一次請求之前執行')


@app.before_request
def handle_before():
    print("在每一次請求之前執行")


@app.after_request
def handle_after(response):
    print("在每一次請求之後執行")
    return response


@app.teardown_appcontext
def handle_teardown(response):
    print('teardown被執行')
    return response


if __name__ == '__main__':
    app.run()

運行後,訪問http://127.0.0.1:5000/,顯示:
flask hook teardown_appcontext
控制檯打印:

在第一次請求之前執行
在每一次請求之前執行
在每一次請求之後執行
teardown被執行
[2020-05-13 12:58:48,206] ERROR in app: Exception on / [GET]
Traceback (most recent call last):
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\_compat.py", line 39, in reraise
    raise value
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "XXX\Test-gftU5mTd\lib\site-packages\flask\app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "XXXX\flask_hook.py", line 8, in index
    1/0
ZeroDivisionError: division by zero
127.0.0.1 - - [13/May/2020 12:58:48] "GET / HTTP/1.1" 500 -

顯然, 在出現異常的情況下也執行了handle_teardown()函數;
虛造關閉Debug模式纔能有明顯的現象,在Debug模式下是不能執行handle_teardown()函數的。

5.context_processor

上下文處理器,返回的字典中的鍵可以在模板上下文中使用。

假如在多個視圖函數中渲染模板時都需要傳入同樣的參數,flask_hook.py如下:

from flask import Flask, render_template

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html', username='Corley')


@app.route('/list/')
def list():
    return render_template('list.html', username='Corley')


if __name__ == '__main__':
    app.run(debug=True)

在模板目錄templates中新建index.html和list.html,index.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首頁</title>
</head>
<body>
    <p>這是首頁</p>
    <p>{{ username }}</p>
</body>
</html>

list.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表</title>
</head>
<body>
    <p>這是列表</p>
    <p>{{ username }}</p>
</body>
</html>

顯示:
flask hook nocontext_processor
顯然,達到了效果,但是在每個視圖函數中都要傳入參數,很麻煩冗餘,可以用context_processor裝飾器定義鉤子函數傳遞公共參數:

from flask import Flask, render_template

app = Flask(__name__)


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


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


@app.context_processor
def context_process():
    return {'username': 'Corley'}


if __name__ == '__main__':
    app.run(debug=True)

效果與之前一樣;
此時在每個視圖函數中不需要再傳遞參數,而是在context_processor裝飾器定義的鉤子函數中返回參數。

6.errorhandler

errorhandler接收狀態碼作爲參數,可以自定義處理返回這個狀態碼的響應的方法。

測試如下:

from flask import Flask, render_template

app = Flask(__name__)


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


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


@app.context_processor
def context_process():
    return {'username': 'Corley'}


@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404


@app.errorhandler(500)
def server_error(error):
    return '<p>服務器內部錯誤...</p>', 500


if __name__ == '__main__':
    app.run()

新建404.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>頁面404</title>
</head>
<body>
    <p>頁面跑到火星去了...</p>
</body>
</html>

顯示:
flask hook errorhandler
顯然,捕獲到了404和500錯誤。

還可以在視圖函數中用abort()方法主動拋出異常,如下:

from flask import Flask, render_template, abort

app = Flask(__name__)


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


@app.route('/list/')
def list():
    abort(404)
    return render_template('list.html')


@app.context_processor
def context_process():
    return {'username': 'Corley'}


@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404


@app.errorhandler(500)
def server_error(error):
    return '<p>服務器內部錯誤...</p>', 500


if __name__ == '__main__':
    app.run()

顯示:
flask hook errorhandler abort
顯然,此時再訪問http://127.0.0.1:5000/list/,就會報錯;
因爲在list()視圖函數中使用sort()方法拋出404錯誤,被鉤子函數page_not_found()捕獲到,因此訪問會出現404錯誤。

五、Flask-Restful

1.Restful API規範

Restful API是用於在前端與後臺進行通信的一套規範,使用這個規範可以讓前後端開發變得更加簡單。

協議

採用http或者https協議。

數據傳輸格式

數據之間傳輸的格式應該使用json形式,而不是xml。

url鏈接

url鏈接中不能有動詞,只能有名詞;
對於一些名詞,如果出現複數,應該在後面加s。

HTTP請求的方法

方法 含義 舉例
GET 從服務器上獲取資源 /users/獲取所有用戶
/user/id/根據id獲取一個用戶
POST 在服務器上新創建一個資源 /user/新建一個用戶
PUT 在服務器上更新資源(客戶端提供所有改變後的數據) /user/id/更新某個id的用戶的信息(需要提供用戶的所有信息)
PATCH 在服務器上更新資源(客戶端只提供需要改變的屬性) /user/id/更新某個id的用戶信息(只需要提供需要改變的信息)
DELETE 從服務器上刪除資源 /user/id/刪除一個用戶

狀態碼

狀態碼 描述 含義
200 OK 服務器成功響應客戶端的請求。
400 INVALID REQUEST 用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作
401 Unauthorized 用戶沒有權限訪問這個請求
403 Forbidden 因爲某些原因禁止訪問這個請求
404 NOT FOUND 用戶發送的請求的url不存在
406 NOT Acceptable 用戶請求不被服務器接收(比如服務器期望客戶端發送某個字段,但是沒有發送)
500 Internal server error 服務器內部錯誤,比如出現了bug

2.Flask-Restful插件

基本概念

Flask-Restful是Flask中專門用來寫restful api的一個插件,使用它可以快速集成restful api功能。
在app的後臺以及純API的後臺中,這個插件可以幫助我們節省很多時間;
在普通的網站中,這個插件顯得有些雞肋,因爲在普通的網頁開發中,是需要去渲染HTML代碼的,而Flask-Restful在每個請求中都返回json格式的數據。

安裝

Flask-Restful的環境要求:

  • Flask版本>=0.80.8
  • Python版本爲2.6、2.7,或3.3及以上。

在虛擬環境中使用pip install flask-restful命令安裝。

基本使用

使用Flask-Restful定義視圖函數時,要繼承自flask_restful.Resource類,再根據具體的請求的方法來定義相應的方法。
例如,如果期望客戶端使用get方法發送請求,那麼就定義一個get方法,類似於MethodView。
簡單使用測試如下:

from flask import Flask
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app)


class IndexView(Resource):
    def get(self):
        return {'username': 'Corley'}

    def post(self):
        return {'info': 'Login Successfully!!'}


api.add_resource(IndexView, '/', endpoint='index')

if __name__ == '__main__':
    app.run(debug=True)

運行之後,使用Postman模擬請求如下:
flask restful simple use
顯然,用get方法和post方法請求得到的結果是不一樣的。
說明:

  • endpoint是用來給url_for()方法反轉url的時候指定的;如果不設置endpoint參數,會使用視圖的名字的小寫作爲endpoint的值。
  • add_resource()方法的第二個參數是視圖函數的路由地址,這個地址與視圖函數的route一樣,可以傳遞參數;有一點不同的是,這個方法可以傳遞多個url來指定視圖函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章