django會話session
因為因特網HTTP協議的特性,每一次來自於使用者瀏覽器的請求(request)都是無狀態的、獨立的。通俗地說,就是無法儲存使用者狀態,後臺伺服器根本就不知道當前請求和以前及以後請求是否來自同一使用者。對於靜態網站,這可能不是個問題,而對於動態網站,尤其是京東、天貓、銀行等購物或金融網站,無法識別使用者並保持使用者狀態是致命的,根本就無法提供服務。你可以嘗試將瀏覽器的cookie功能關閉,你會發現將無法在京東登入和購物。
為了保持連線狀態,網站會通過使用者的瀏覽器在使用者機器內被限定的硬碟位置中寫入一些資料,也就是所謂的Cookie。通過Cookie可以儲存一些諸如使用者名稱、瀏覽記錄、表單記錄、登入和登出等各種資料。但是這種方式非常不安全,因為Cookie儲存在使用者的機器上,如果Cookie被偽造、篡改或刪除,就會造成極大的安全威脅,因此,現代網站設計通常將Cookie用來儲存一些不重要的內容,實際的使用者資料和狀態還是以Session會話的方式儲存在伺服器端。
Session就是在伺服器端的‘Cookie’,將使用者資料儲存在伺服器端,遠比儲存在使用者端要安全、方便和快捷得多。
Session依賴Cookie!但與Cookie不同的地方在於Session將所有的資料都放在伺服器端,使用者瀏覽器的Cookie中只會儲存一個非明文的識別資訊,比如雜湊值。
Session是大多數網站都需要具備的功能。Django為我們提供了一個通用的Session框架,並且可以使用多種session資料的儲存方式:
- 儲存在資料庫內
- 儲存到快取
- 儲存到檔案內
- 儲存到cookie內
通常情況,沒有特別需求的話,請使用儲存在資料庫內的方式,儘量不要儲存到Cookie內。
Django的session框架支援匿名會話,封裝了cookies的傳送和接收過程。cookie包含一個會話ID而不是資料本身(除非你使用的是基於後端的cookie)。
Django的會話框架完全地、唯一地基於Cookie。它不像PHP一樣,把會話的ID放在URL中。那樣不僅使得URL變得醜陋,還使得你的網站易於受到通過"Referer"頭部進行竊取會話ID的攻擊。
一、啟用會話
Django通過一個內建中介軟體來實現會話功能。要啟用會話就要先啟用該中介軟體。編輯settings.py中的MIDDLEWARE設定,確保存在django.contrib.sessions.middleware.SessionMiddleware
如果你不想使用會話功能,那麼在settings檔案中,將SessionMiddleware從MIDDLEWARE中刪除,將django.contrib.sessions
從INSTALLED_APPS
中刪除就OK了。
二、配置會話引擎
預設情況下,Django將會話資料儲存在資料庫內(通過使用django.contrib.sessions.models.Session
模型)。當然,你也可以將資料儲存在檔案系統或快取內。
1. 基於資料庫的會話
確保在INSTALLED_APPS
設定中django.contrib.sessions的存在,然後執行manage.py migrate
命令在資料庫內建立sessions表。
2. 基於快取的會話
從效能角度考慮,基於快取的會話會更好一些。但是首先,你得先配置好你的快取。
如果你定義有多個快取,Django將使用預設的那個。如果你想用其它的,請將SESSION_CACHE_ALIAS
引數設定為那個快取的名字。
配置好快取後,你可以選擇兩種儲存資料的方法:
- 一是將
SESSION_ENGINE
設定為"django.contrib.sessions.backends.cache",簡單的對會話進行儲存。但是這種方法不是很可靠,因為當快取資料存滿時將清除部分資料,或者遇到快取伺服器重啟時資料將丟失。 - 為了資料安全保障,可以將
SESSION_ENGINE
設定為"django.contrib.sessions.backends.cached_db"。這種方式在每次快取的時候會同時將資料在資料庫內寫一份。當快取不可用時,會話會從資料庫內讀取資料。
兩種方法都很迅速,但是第一種簡單的快取更快一些,因為它忽略了資料的永續性。如果你使用快取+資料庫的方式,還需要對資料庫進行配置。
3. 基於檔案的會話
將SESSION_ENGINE
設定為"django.contrib.sessions.backends.file"。同時,你必須正確配置SESSION_FILE_PATH
(預設使用tempfile.gettempdir()方法的返回值,就像/tmp目錄),確保你的檔案儲存目錄,以及Web伺服器對該目錄具有讀寫許可權。
4. 基於cookie的會話
將SESSION_ENGINE
設定為"django.contrib.sessions.backends.signed_cookies"。Django將使用加密簽名工具和安全祕鑰設定儲存會話的cookie。
注意:建議將SESSION_COOKIE_HTTPONLY
設定為True,阻止javascript對會話資料的訪問,提高安全性。
三、在檢視中使用會話
當會話中介軟體啟用後,傳遞給檢視request引數的HttpRequest物件將包含一個session屬性,這個屬性的值是一個類似字典的物件。
你可以在檢視的任何地方讀寫request.session屬性,或者多次編輯使用它。
class backends.base.SessionBase # 這是所有會話物件的基類,包含標準的字典方法: __getitem__(key) Example: fav_color = request.session['fav_color'] __setitem__(key, value) Example: request.session['fav_color'] = 'blue' __delitem__(key) Example: del request.session['fav_color'] # 如果不存在會丟擲異常 __contains__(key) Example: 'fav_color' in request.session get(key, default=None) Example: fav_color = request.session.get('fav_color', 'red') pop(key, default=__not_given) Example: fav_color = request.session.pop('fav_color', 'blue')
# 類似字典資料型別的內建方法 keys() items() setdefault() clear() # 它還有下面的方法: flush() # 刪除當前的會話資料和會話cookie。經常用在使用者退出後,刪除會話。 set_test_cookie() # 設定一個測試cookie,用於探測使用者瀏覽器是否支援cookies。由於cookie的工作機制,你只有在下次使用者請求的時候才可以測試。 test_cookie_worked() # 返回True或者False,取決於使用者的瀏覽器是否接受測試cookie。你必須在之前先呼叫set_test_cookie()方法。 delete_test_cookie() # 刪除測試cookie。 set_expiry(value) # 設定cookie的有效期。可以傳遞不同型別的引數值: • 如果值是一個整數,session將在對應的秒數後失效。例如request.session.set_expiry(300) 將在300秒後失效. • 如果值是一個datetime或者timedelta物件, 會話將在指定的日期失效 • 如果為0,在使用者關閉瀏覽器後失效 • 如果為None,則將使用全域性會話失效策略 失效時間從上一次會話被修改的時刻開始計時。 get_expiry_age() # 返回多少秒後失效的秒數。對於沒有自定義失效時間的會話,這等同於SESSION_COOKIE_AGE. # 這個方法接受2個可選的關鍵字引數 • modification:會話的最後修改時間(datetime物件)。預設是當前時間。 •expiry: 會話失效資訊,可以是datetime物件,也可以是int或None get_expiry_date() # 和上面的方法類似,只是返回的是日期 get_expire_at_browser_close() # 返回True或False,根據使用者會話是否是瀏覽器關閉後就結束。 clear_expired() # 刪除已經失效的會話資料。 cycle_key() # 建立一個新的會話祕鑰用於保持當前的會話資料。django.contrib.auth.login() 會呼叫這個方法。
1. 序列化會話
Django預設使用JSON序列化會話資料。你可以在SESSION_SERIALIZER
設定中自定義序列化格式,甚至寫入警告說明。但是強烈建議你還是使用JSON,尤其是以cookie的方式進行會話時。
舉個例子,一個使用pickle序列化會話資料的攻擊場景。如果你使用的是已簽名的Cookie會話並且SECRET_KEY
被攻擊者知道了(通過其它手段),攻擊者就可以在會話中插入一個字串,在pickle反序列化時,可以在伺服器上執行危險的程式碼。在因特網上這個攻擊技術很簡單並很容易使用。儘管Cookie會話會對資料進行簽名以防止篡改,但是SECRET_KEY
的洩漏卻使得一切前功盡棄。
內建的序列化方法
1.class serializers.JSONSerializer
對django.core.signing中JSON序列化方法的一個包裝。只可以序列化基本的資料型別。另外,JSON只支援以字串作為鍵值,使用其它的型別會導致異常。
>>> # initial assignment >>> request.session[0] = 'bar' >>> # subsequent requests following serialization & deserialization >>> # of session data >>> request.session[0] # KeyError >>> request.session['0'] 'bar'
同樣,無法被JSON編碼的,例如非UTF8格式的位元組’\xd9’一樣是無法被儲存的,它會導致UnicodeDecodeError異常。
2.class serializers.PickleSerializer
支援任意型別的Python物件,但是就像前面說的,可能導致遠端執行程式碼的漏洞,如果攻擊者知道了SECRET_KEY
。
自定義序列化方法
自定義的序列化類必須分別實現dumps(self, obj)
和loads(self, data)
方法,用來實現序列化和反序列化會話資料字典。
2. 會話使用中的一些建議
- 使用普通的Python字串作為request.session字典的鍵值。這不是一條硬性規則而是為方便起見。
- 以一個下劃線開始的會話字典的鍵被Django保留作為內部使用。
- 不要用新物件覆蓋request.session,不要直接訪問或設定它的屬性。像一個Python字典一樣的使用它。
3. 會話使用範例
下面這個簡單的檢視在使用者發表評論後,在session中設定一個has_commented
變數為True。它不允許使用者重複發表評論。
def post_comment(request, new_comment): if request.session.get('has_commented', False): return HttpResponse("You've already commented.") c = comments.Comment(comment=new_comment) c.save() request.session['has_commented'] = True return HttpResponse('Thanks for your comment!')
下面是一個簡單的使用者登入檢視:
def login(request):
m = Member.objects.get(username=request.POST['username']) if m.password == request.POST['password']: request.session['member_id'] = m.id return HttpResponse("You're logged in.") else: return HttpResponse("Your username and password didn't match.")
下面則是一個退出登入的檢視,與上面的相關:
def logout(request):
try: del request.session['member_id'] except KeyError: pass return HttpResponse("You're logged out.")
Django內建的django.contrib.auth.logout()
函式實際上所做的內容比上面的例子要更嚴謹,以防止意外的資料洩露,它會呼叫request.session的flush()方法。我們使用這個例子只是演示如何利用會話物件來工作,而不是一個完整的logout()實現。
四、測試cookie
為了方便,Django 提供一個簡單的方法來測試使用者的瀏覽器是否接受Cookie。只需在一個檢視中呼叫request.session
的set_test_cookie()
方法,並在隨後的檢視中呼叫test_cookie_worked()
獲取測試結果(True或False)。注意,不能在同一個檢視中呼叫這兩個方法。
造成這種分割呼叫的原因是cookie的工作機制。當你設定一個cookie時,你無法立刻得到結果,直到瀏覽器傳送下一個請求時才能獲得結果。
在測試後,記得使用delete_test_cookie()
方法清除測試資料。
下面是一個典型的範例:
from django.http import HttpResponse
from django.shortcuts import render def login(request): if request.method == 'POST': if request.session.test_cookie_worked(): request.session.delete_test_cookie() return HttpResponse("You're logged in.") else: return HttpResponse("Please enable cookies and try again.") request.session.set_test_cookie() return render(request, 'foo/login_form.html')
五、在檢視外使用session
在下面的例子中,我們直接從django.contrib.sessions.backends.db中匯入了SessionStore物件。在你的實際程式碼中,應該採用下面的匯入方法,根據SESSION_ENGINE
的設定進行匯入,如下所示:
>>> from importlib import import_module
>>> from django.conf import settings >>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
在檢視外可以通過下面的API操作會話資料:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore() >>> # stored as seconds since epoch since datetimes are not serializable in JSON. >>> s['last_login'] = 1376587691 >>> s.create() >>> s.session_key '2b1189a188b44ad18c35e113ac6ceead' >>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead') >>> s['last_login'] 1376587691
SessionStore.create()
用於建立一個新的會話。save()
方法用於儲存一個已經存在的會話。create方法會呼叫save方法並迴圈直到生成一個未使用的session_key
。直接呼叫save方法也可以建立一個新的會話,但在生成session_key
的時候有可能和已經存在的發生衝突。
如果你使用的是django.contrib.sessions.backends.db模式,那麼每一個會話其實就是一個普通的Django模型,你可以使用普通的Django資料庫API訪問它。會話模型的定義在django/contrib/sessions/models.py
檔案裡。例如:
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead') >>> s.expire_date datetime.datetime(2005, 8, 20, 13, 35, 12)
注意,需要呼叫get_decoded()
方法才能獲得會話字典,因為字典是採用編碼格式儲存的。如下:
>>> s.session_data 'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...' >>> s.get_decoded() {'user_id': 42}
六、會話的儲存機制
預設情況下,只有當會話字典的某個值被重新設定或刪除的時候,Django才會將會話內容儲存到會話資料庫內。
# 會話被修改 request.session['foo'] = 'bar' # 會話被刪除 del request.session['foo'] # 會話被修改 request.session['foo'] = {} # 會話沒有被修改,只是修改了request.session['foo'] request.session['foo']['bar'] = 'baz'
要理解上面最後一種情況有點費勁。我們可以通過設定會話物件的modified屬性值,顯式地告訴會話物件它已經被修改過:request.session.modified = True
。
要改變上面的預設行為,將SESSION_SAVE_EVERY_REQUEST
設定為True,那麼每一次單獨的請求過來,Django都會儲存會話到資料庫。
注意,會話的Cookie只有在一個會話被建立或修改後才會再次傳送。如果SESSION_SAVE_EVERY_REQUEST
為True,每次請求都會發送cookie。
類似地,會話Cookie的失效部分在每次傳送會話Cookie時都會更新。
如果響應的狀態碼為500,則會話不會被儲存。
七、會話生存期
預設情況下,SESSION_EXPIRE_AT_BROWSER_CLOSE
設定為False,也就是說cookie儲存在使用者的瀏覽器內,直到失效日期,這樣使用者就不必每次開啟瀏覽器後都要再登入一次。
相反的,如果將SESSION_EXPIRE_AT_BROWSER_CLOSE
設定為True,則意味著瀏覽器一關閉,cookie就失效,每次重新開啟瀏覽器,你就得重新登入。
這個設定是一個全域性的預設值,可以通過顯式地調request.session
的set_expiry()
方法來覆蓋,前面我們已經描述過了。
注意:有些瀏覽器(比如Chrome)具有在關閉後重新開啟瀏覽器,會話依然保持的功能。這會與Django的SESSION_EXPIRE_AT_BROWSER_CLOSE
設定發生衝突。請一定要小心。
八、清除已儲存的會話
隨著使用者的訪問,會話資料會越來越龐大。如果你使用的是資料庫儲存模式,那麼django_session
表的內容會逐漸增長。如果你使用的是檔案模式,那麼你的臨時目錄內的檔案數量會不斷增加。
造成這個問題的原因是,如果使用者手動退出登入,Django將會自動刪除會話資料,但是如果使用者不退出登入,那麼對應的會話資料不會被刪除。
Django沒有提供自動清除失效會話的機制。因此,你必須自己完成這項工作。但是Django提供了一個命令clearsessions
用於清除會話資料,建議你基於這個命令設定一個週期性的自動清除機制,比如crontab或者Windows的排程任務。
不同的是,使用快取模式的會話不需要你清理資料,因為快取系統自己有清理過期資料的機制。使用cookie模式的會話也不需要,因為資料都存在使用者的瀏覽器內,不用你幫忙。
九、會話的相關設定
下面是Django的session相關設定,用於幫助你控制會話的行為,大多數在前面都介紹過了:
- SESSION_CACHE_ALIAS
- SESSION_COOKIE_AGE
- SESSION_COOKIE_DOMAIN
- SESSION_COOKIE_HTTPONLY
- SESSION_COOKIE_NAME
- SESSION_COOKIE_PATH
- SESSION_COOKIE_SECURE
- SESSION_ENGINE
- SESSION_EXPIRE_AT_BROWSER_CLOSE
- SESSION_FILE_PATH
- SESSION_SAVE_EVERY_REQUEST
- SESSION_SERIALIZER
十、SessionStore物件
在會話內部,Django使用一個與會話引擎對應的會話儲存物件。這個會話儲存物件命名為SessionStore,位於SESSION_ENGINE
設定指定的模組內。
所有Django支援的SessionStore類都繼承SessionBase類,並實現了下面的資料操作方法:
- exists()
- create()
- save()
- delete()
- load()
- clear_expored()
為了建立一個自定義會話引擎或修改一個現成的引擎,你需要建立一個新的類,它繼承SessionBase類或任何其他已經存在的SessionStore類。