會話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類。
會話session