Flask session 概念:
程式可以把資料存儲在使用者會話中,使用者會話是-種私有存儲,預設情況下,它會儲存在用戶端cookie中。Flask提供了session 對
象來操作使用者會話。session 是基于cookie實作, 儲存在服務端的鍵值對(形式為 {随機字元串:‘xxxxxx’}), 同時在浏覽器中的cookie中也對應一相同的随機字元串,用來再次請求的 時候驗證;
注意 :Flask中的session是存在浏覽器中 預設key是session(加密的cookie),使用session時要設定一個密鑰app. secret_ key
操作session就像操作python 中的字典一樣,我們可以使用session[‘user’]擷取值,也可以使用session. get(‘user’)擷取值。
因為session就像字典一樣是以,操作它的時候有兩種方法:
(1)result = session[‘username’] :如果内容不存在,将會報異常
(2)result = session.get(‘username’) :如果内容不存在,将傳回None(推薦用法)
是以,使用第二種方法擷取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(days=7) # 配置7天有效
# 設定session
@app.route('/')
def set():
session['username'] = 'zhangvalue' # 設定“字典”鍵值對
session.permanent = True # 設定session的有效時間,長期有效,一個月的時間有效,
# 具體看上面的配置時間具體的,沒有上面設定的時間就是一個月有效
print(app.config['SECRET_KEY'])
return 'success'
# 讀取session
@app.route('/get')
def get():
# 第一種session擷取如果不存在會報錯
# session['username']
# 推薦使用session.get('username')
# session.get('username')
return session.get('username')
# 删除session
@app.route('/delete/')
def delete():
print(session.get('username'), session.pop('username', None))
# 或者 session['username'] = False
print(session.get('username'))
return 'success'
# 清除session中所有資料
@app.route('/clear')
def clear():
print(session.get('username'))
# 清除session中所有資料
session.clear
print(session.get('username'))
return 'success'
if __name__ == '__main__':
app.run(host='0.0.0.0', port='5010')
在擷取session的時候如果如果使用第一種方法,不存在經就會報錯,因為不存在key為username2
設定session的過期時間
如果沒有指定session的過期時間,那麼預設是浏覽器關閉後就自動結束。
session.permanent = True在flask下則可以将有效期延長至一個月。下面有方法可以配置具體多少天的有效期。
如果沒有指定session的過期時間,那麼預設是浏覽器關閉後就自動結束
如果設定了session的permanent屬性為True,那麼過期時間是31天。
可以通過給app.config設定PERMANENT_SESSION_LIFETIME來更改過期時間,這個值的資料類型是datetime.timedelay類型。
一種更先進的配置有效期的方法:(比如配置7天有效)
1.引入包:from datetime import timedelta
2.配置有效期限:app.config[‘PERMANENT_SESSION_LIFETIME’] = timedelta(days=7) # 配置7天有效
3.設定:session.permanent = True
再次認識session
flask的session是基于cookie的會話保持。簡單的原理即:
當用戶端進行第一次請求時,用戶端的HTTP request(cookie為空)到服務端,服務端建立session,視圖函數根據form表單填寫session,請求結束時,session内容填寫入response的cookie中并傳回給用戶端,用戶端的cookie中便儲存了使用者的資料。
當同一用戶端再次請求時, 用戶端的HTTP request中cookie已經攜帶資料,視圖函數根據cookie中值做相應操作(如已經攜帶使用者名和密碼就可以直接登陸)。
在 flask 中使用 session 也很簡單,隻要使用 from flask import session 導入這個變量,在代碼中就能直接通過讀寫它和 session 互動。
from flask import Flask, session, escape, request
app = Flask(__name__)
app.secret_key = 'please-generate-a-random-secret_key'
@app.route("/")
def index():
if 'username' in session:
return 'hello, {}\n'.format(escape(session['username']))
return 'hello, stranger\n'
@app.route("/login", methods=['POST'])
def login():
session['username'] = request.form['username']
return 'login success'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
上面這段代碼模拟了一個非常簡單的登陸邏輯,使用者通路 POST /login 來登陸,後面通路頁面的時候 GET /,會傳回該使用者的名字。
flask中session使用非常簡單,但是實作原理卻沒那麼簡單,下面我們通過幾個問題來弄清楚session是如何實作的。
二、請求第一次來時,session是什麼時候生成的?存放在哪裡?
在flask學習筆記–請求上下文和應用上下文中已經知道session是一個LocalProxy()對象:
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
用戶端的請求進來時,會調用app.wsgi_app():
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
# 尋找視圖函數,并執行
# 擷取傳回值 response
response = self.full_dispatch_request()
此時,會生成一個ctx,其本質是一個RequestContext對象:
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.flashes = None
self.session = None
在RequestContext 對象中定義了session,且初值為None。
接着繼續看wsgi_app函數中,ctx.push()函數:
def push(self):
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(
self.app, self.request
)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
前半部分代碼已經在之前的文章中講到,主要看後半部分代碼。判斷session是否為空,我在RequestContext 中看到session初值為空.
在 Flask 中,所有和 session 有關的調用,都是轉發到 self.session_interface 的方法調用上(這樣使用者就能用自定義的 session_interface 來控制 session 的使用)。而預設的 session_inerface 有預設值:
執行SecureCookieSessionInterface.open_session()來生成預設session對象:
def open_session(self, app, request):
擷取session簽名的算法
s = self.get_signing_serializer(app)
如果為空 直接傳回None
if s is None:
return None
val = request.cookies.get(app.session_cookie_name)
# 如果val為空,即request.cookies為空
if not val:
return self.session_class()
max_age = total_seconds(app.permanent_session_lifetime)
try:
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
return self.session_class()
請求第一次來時,request.cookies為空,即傳回self.session_class():
session_class = SecureCookieSession
看SecureCookieSession:
class SecureCookieSession(CallbackDict, SessionMixin):
modified = False
accessed = False
def __init__(self, initial=None):
def on_update(self):
self.modified = True
self.accessed = True
super(SecureCookieSession, self).__init__(initial, on_update)
def __getitem__(self, key):
self.accessed = True
return super(SecureCookieSession, self).__getitem__(key)
def get(self, key, default=None):
self.accessed = True
return super(SecureCookieSession, self).get(key, default)
def setdefault(self, key, default=None):
self.accessed = True
return super(SecureCookieSession, self).setdefault(key, default)
看其繼承關系,其實就是一個特殊的字典。到此我們知道了session就是一個特殊的字典,調用SecureCookieSessionInterface類的open_session()建立,并儲存在ctx中,即RequestContext對象中。但最終由session = LocalProxy(…, ‘session’)對象代為管理,到此,在視圖函數中就可以導入session并使用了。
三、當請求第二次來時,session生成的是什麼?
當請求第二次到來時,與第一次的不同就在open_session()那個val判斷處,此時cookies不為空, 擷取cookie的有效時長,如果cookie依然有效,通過與寫入時同樣的簽名算法将cookie中的值解密出來并寫入字典并傳回中,若cookie已經失效,則仍然傳回’空字典’。
四、特殊的SecureCookieSession字典有那些功能?如何實作的?
預設的 session 對象是 SecureCookieSession,這個類就是一個基本的字典,外加一些特殊的屬性,比如 permanent(flask 插件會用到這個變量)、modified(表明執行個體是否被更新過,如果更新過就要重新計算并設定 cookie,因為計算過程比較貴,是以如果對象沒有被修改,就直接跳過)。
怎麼知道執行個體的資料被更新過呢? SecureCookieSession 是基于 werkzeug/datastructures:CallbackDict 實作的,這個類可以指定一個函數作為 on_update 參數,每次有字典操作的時候(setitem、delitem、clear、popitem、update、pop、setdefault)會調用這個函數。
SecureCookieSession:
class SecureCookieSession(CallbackDict, SessionMixin):
modified = False
accessed = False
def __init__(self, initial=None):
def on_update(self):
self.modified = True
self.accessed = True
#将on_update()傳遞給CallbackDict
super(SecureCookieSession, self).__init__(initial, on_update)
def __getitem__(self, key):
self.accessed = True
return super(SecureCookieSession, self).__getitem__(key)
def get(self, key, default=None):
self.accessed = True
return super(SecureCookieSession, self).get(key, default)
def setdefault(self, key, default=None):
self.accessed = True
return super(SecureCookieSession, self).setdefault(key, default)
繼承的 CallbackDict:
class CallbackDict(UpdateDictMixin, dict):
def __init__(self, initial=None, on_update=None):
dict.__init__(self, initial or ())
self.on_update = on_update
def __repr__(self):
return '<%s %s>' % (
self.__class__.__name__,
dict.__repr__(self)
)
CallbackDict又繼承UpdateDictMixin:
class UpdateDictMixin(object):
on_update = None
def calls_update(name):
def oncall(self, *args, **kw):
rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw)
if self.on_update is not None:
self.on_update(self)
return rv
oncall.__name__ = name
return oncall
def setdefault(self, key, default=None):
modified = key not in self
rv = super(UpdateDictMixin, self).setdefault(key, default)
if modified and self.on_update is not None:
self.on_update(self)
return rv
def pop(self, key, default=_missing):
modified = key in self
if default is _missing:
rv = super(UpdateDictMixin, self).pop(key)
else:
rv = super(UpdateDictMixin, self).pop(key, default)
if modified and self.on_update is not None:
self.on_update(self)
return rv
__setitem__ = calls_update('__setitem__')
__delitem__ = calls_update('__delitem__')
clear = calls_update('clear')
popitem = calls_update('popitem')
update = calls_update('update')
del calls_update
由UpdateDictMixin()可知,對session進行改動會調用pop, __setitem__等方法,同時就會調用on_update()方法,進而修改
modify,security的值。
五、簽名算法:
都擷取 cookie 資料的過程中,最核心的幾句話是:
s = self.get_signing_serializer(app)
val = request.cookies.get(app.session_cookie_name)
data = s.loads(val, max_age=max_age)
return self.session_class(data)
其中兩句都和 s 有關,signing_serializer 保證了 cookie 和 session 的轉換過程中的安全問題。如果 flask 發現請求的 cookie 被篡改了,它會直接放棄使用。
我們繼續看 get_signing_serializer 方法:
def get_signing_serializer(self, app):
if not app.secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation,
digest_method=self.digest_method
)
return URLSafeTimedSerializer(app.secret_key,
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs)
我們看到這裡需要用到很多參數:
secret_key:密鑰。這個是必須的,如果沒有配置 secret_key 就直接使用 session 會報錯
salt:為了增強安全性而設定一個 salt 字元串(可以自行搜尋“安全加鹽”了解對應的原理)
serializer:序列算法
signer_kwargs:其他參數,包括摘要/hash算法(預設是 sha1)和 簽名算法(預設是 hmac)
URLSafeTimedSerializer 是 itsdangerous 庫的類,主要用來進行資料驗證,增加網絡中資料的安全性。itsdangerours提供了多種 Serializer,可以友善地進行類似 json 處理的資料序列化和反序列的操作。至于具體的實作,因為篇幅限制,就不解釋了。
六、session什麼時候寫入cookie中?session的生命周期?
前面的幾個問題實際上都發生在wsgi_app()前兩句函數中,主要就是ctx.push()函數中,下面看看wsgi_app()後面幹了嘛:
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
# ctx.push函數是前半部分最重要的一個函數
# 生成request和session并将二者儲存到RequestContext()對象ctxz中
# 最後将ctx,push到LocalStack()對象_request_ctx_stack中
ctx.push()
# 尋找視圖函數,并執行
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 最後, 将自己請求在local中的資料清除
ctx.auto_pop(error)
看full_dispatch_request:
def full_dispatch_request(self):
#執行before_first_request
self.try_trigger_before_first_request_functions()
try:
# 觸發request_started 信号
request_started.send(self)
# 調用before_request
rv = self.preprocess_request()
if rv is None:
#執行視圖函數
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)
前半部分就在執行flask鈎子,before_first_request, before_request以及信号,接着執行視圖函數生成rv,我們主要看finalize_request(rv):
def finalize_request(self, rv, from_error_handler=False):
response = self.make_response(rv)
try:
response = self.process_response(response)
request_finished.send(self, response=response)
except Exception:
if not from_error_handler:
raise
self.logger.exception('Request finalizing failed with an '
'error while handling an error')
return response
首先根據rv生成response。再執行process_response:
def process_response(self, response):
ctx = _request_ctx_stack.top
bp = ctx.request.blueprint
funcs = ctx._after_request_functions
if bp is not None and bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response)
return response
前半部分主要執行flask的鈎子,看後面,判斷,session是否為空,如果不為空,則執行save_session():
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
# If the session is modified to be empty, remove the cookie.
# If the session is empty, return without setting the cookie.
if not session:
if session.modified:
response.delete_cookie(
app.session_cookie_name,
domain=domain,
path=path
)
return
# Add a "Vary: Cookie" header if the session was accessed at all.
if session.accessed:
response.vary.add('Cookie')
if not self.should_set_cookie(app, session):
return
httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
samesite = self.get_cookie_samesite(app)
expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie(
app.session_cookie_name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite
)
ave_session()比較簡單,且有注釋,便不再講解,主要就是将session寫入response.set_cookie中。這樣便完成session的寫入response工作,并由response傳回至用戶端。
再請求結束時會執行wsgi_app()的finally:ctx.auto_pop(error)函數,将與對應請求相關的request,session清除,session生命周期便結束。
總結:
至此,flask内置session的機制便講解完畢,session的實作是依賴與flask的上下文管理,是以先弄清楚flask上下文,再來看session就比較容易了解。其主要的就是SecureCookieSessionInterface對象的open_session()與save_session() 。open_session在請求剛進來時執行,完成session對象的建立(就是一特殊字典),在視圖函數中完成對session的指派操作,save_session()在視圖函數執行完後,生成response後執行,将session寫入response的cookie中。
當然,flask内置session無法滿足生産需求。因為将session資料全部儲存在cookie中不安全且cookie存儲資料量有限,但flask-session元件幫我們實作了将資料儲存在伺服器’‘資料庫’'中而隻将sessionID儲存在cookie中
session.load()和session.get()差別
get先到緩存中去查如果沒有就到DB中去查(即馬上發出sql語句)總之如果你确定DB中有這個對象就用load()不确定就用get()這樣效率會高