Django 提供對匿名會話的完全支持。其會話框架讓你根據各個站點的訪問者存儲和訪問任意數據。它在服務器端存儲數據并抽象Cookie 的發(fā)送和接收。Cookie 包含會話的ID —— 不是數據本身(除非你使用基于Cookie 的后端)。
會話是通過一個中間件實現(xiàn)的。
為了啟用會話功能,需要這樣做:
編輯MIDDLEWARE_CLASSES 設置并確保它包含'django.contrib.sessions.middleware.SessionMiddleware'。django-admin startproject創(chuàng)建的默認的settings.py已經啟用SessionMiddleware。
如果你不想使用會話,你也可以從MIDDLEWARE_CLASSES中刪除SessionMiddleware行,并從INSTALLED_APPS中刪除'django.contrib.sessions'。它將節(jié)省一些性能消耗。
默認情況下,Django 存儲會話到你的數據庫中(使用django.contrib.sessions.models.Session模型)。雖然這很方便,但是在某些架構中存儲會話在其它地方會更快,所以可以配置Django 來存儲會話到你的文件系統(tǒng)上或緩存中。
如果你想使用數據庫支持的會話,你需要添加'django.contrib.sessions' 到你的INSTALLED_APPS設置中。
在配置完成之后,請運行manage.py migrate來安裝保存會話數據的一張數據庫表。
為了更好的性能,你可能想使用一個基于緩存的會話后端。
為了使用Django 的緩存系統(tǒng)來存儲會話數據,你首先需要確保你已經配置好你的緩存;詳細信息參見緩存的文檔。
警告
你應該只在使用Memcached 緩存系統(tǒng)時才使用基于緩存的會話?;诒镜貎却娴木彺嫦到y(tǒng)不會長時間保留數據,所以不是一個好的選擇,而且直接使用文件或數據庫會話比通過文件或數據庫緩存系統(tǒng)要快。另外,基于本地內存的緩存系統(tǒng)不是多進程安全的,所以對于生產環(huán)境可能不是一個好的選擇。
如果你在CACHES中定義多個緩存,Django 將使用默認的緩存。若要使用另外一種緩存,請設置SESSION_CACHE_ALIAS為該緩存的名字。
配置好緩存之后,對于如何在緩存中存儲數據你有兩個選擇:
SESSION_ENGINE 為"django.contrib.sessions.backends.cache" 。此時會話數據將直接存儲在你的緩存中。然而,緩存數據將可能不會持久:如果緩存填滿或者緩存服務器重啟,緩存數據可能會被清理掉。SESSION_ENGINE為"django.contrib.sessions.backends.cached_db"。它的寫操作使用緩存 —— 對緩存的每次寫入都將再寫入到數據庫。對于讀取的會話,如果數據不在緩存中,則從數據庫讀取。兩種會話的存儲都非??欤呛唵蔚木彺娓?,因為它放棄了持久性。大部分情況下,cached_db后端已經足夠快,但是如果你需要榨干最后一點的性能,并且接收讓會話數據丟失,那么你可使用cache后端。
如果你使用cached_db 會話后端,你還需要遵循使用數據庫支持的會話中的配置說明。
Changed in Django 1.7:
在1.7 版之前,`cached_db` 永遠使用`default`緩存而不是`SESSION_CACHE_ALIAS`。
要使用基于文件的緩存,請設置SESSION_ENGINE為"django.contrib.sessions.backends.file"。
你可能還想設置SESSION_FILE_PATH(它的默認值來自tempfile.gettempdir()的輸出,大部分情況是/tmp)來控制Django在哪里存儲會話文件。請保證你的Web 服務器具有讀取和寫入這個位置的權限。
要使用基于Cookie 的會話,請設置SESSION_ENGINE 為"django.contrib.sessions.backends.signed_cookies"。此時,會話數據的存儲將使用Django 的加密簽名 工具和SECRET_KEY 設置。
注
建議保留SESSION_COOKIE_HTTPONLY 設置為True 以防止從JavaScript 中訪問存儲的數據。
警告
如果
SECRET_KEY沒有保密并且你正在使用PickleSerializer,這可能導致遠端執(zhí)行任意的代碼。擁有
SECRET_KEY的攻擊者不僅可以生成篡改的會話數據而你的站點將會信任這些數據,而且可以遠程執(zhí)行任何代碼,就像數據是通過pickle 序列化過的一樣。如果你使用基于Cookie 的會話,請格外注意你的安全秘鑰對于任何可以遠程訪問的系統(tǒng)都是永遠完全保密的。
會話數據經過簽名但沒有加密。
如果使用基于Cookie的會話,則會話數據可以被客戶端讀取。
MAC(消息認證碼)被用來保護數據不被客戶端修改,所以被篡改的會話數據將是變成不合法的。如果保存Cookie的客戶端(例如你的瀏覽器)不能保存所有的會話Cookie或丟失數據,會話同樣會變得不合法。盡管Django 對數據進行壓縮,仍然完全有可能超過每個Cookie 常見的4096 個字節(jié)的限制。
沒有更新保證
還要注意,雖然MAC可以保證數據的權威性(由你的站點生成,而不是任何其他人)和完整性(包含全部的數據并且是正確的),它不能保證是最新的,例如返回給你發(fā)送給客戶端的最新的數據。這意味著對于某些會話數據的使用,基于Cookie 可能讓你受到重放攻擊。其它方式的會話后端在服務器端保存每個會話并在用戶登出時使它無效,基于Cookie 的會話在用戶登出時不會失效。因此,如果一個攻擊者盜取用戶的Cookie,它們可以使用這個Cookie 來以這個用戶登錄即使用戶已登出。Cookies 只能被當做是“過期的”,如果它們比你的SESSION_COOKIE_AGE要舊。
性能
最后,Cookie 的大小對你的網站的速度 有影響。
當SessionMiddleware 激活時,每個HttpRequest 對象 —— 傳遞給Django 視圖函數的第一個參數 —— 將具有一個session 屬性,它是一個類字典對象。
你可以在你的視圖中任何地方讀取并寫入 request.session。你可以多次編輯它。
class backends.base.SessionBase
這是所有會話對象的基類。它具有以下標準的字典方法:
__getitem__(key)
例如:fav_color = request.session['fav_color']
__setitem__(key, value)
例如:request.session['fav_color'] = 'blue'
__delitem__(key)
例如:del request.session['fav_color']。如果給出的key 在會話中不存在,將拋出 KeyError。
__contains__(key)
例如:'fav_color' in request.session
get(key, default=None)
例如:fav_color = request.session.get('fav_color', 'red')
pop(key)
例如:fav_color = request.session.pop('fav_color')
keys()
items()
setdefault()
clear()
它還具有這些方法:
flush()
刪除當前的會話數據并刪除會話的Cookie。這用于確保前面的會話數據不可以再次被用戶的瀏覽器訪問(例如,django.contrib.auth.logout() 函數中就會調用它)。
Changed in Django 1.8:
刪除會話Cookie 是Django 1.8 中的新行為。以前,該行為用于重新生成會話中的值,這個值會在Cookie 中發(fā)回給用戶。
set_test_cookie()
設置一個測試的Cookie 來驗證用戶的瀏覽器是否支持Cookie。因為Cookie 的工作方式,只有到用戶的下一個頁面才能驗證。更多信息參見下文的設置測試的Cookie。
test_cookie_worked()
返回True 或False,取決于用戶的瀏覽器時候接受測試的Cookie。因為Cookie的工作方式,你必須在前面一個單獨的頁面請求中調用set_test_cookie()。更多信息參見下文的設置測試的Cookie。
delete_test_cookie()
刪除測試的Cookie。使用這個函數來自己清理。
set_expiry(value)
設置會話的超時時間。你可以傳遞一系列不同的值:
value 是一個整數,會話將在這么多秒沒有活動后過期。例如,調用request.session.set_expiry(300) 將使得會話在5分鐘后過期。datetime 或timedelta 對象,會話將在這個指定的日期/時間過期。注意datetime 和timedelta 值只有在你使用PickleSerializer 時才可序列化。value 為0,那么用戶會話的Cookie將在用戶的瀏覽器關閉時過期。value 為None,那么會話轉向使用全局的會話過期策略。過期的計算不考慮讀取會話的操作。會話的過期從會話上次修改的時間開始計算。
get_expiry_age()
返回會話離過期的秒數。對于沒有自定義過期的會話(或者設置為瀏覽器關閉時過期的會話),它將等于SESSION_COOKIE_AGE。
該函數接收兩個可選的關鍵字參數:
modification:會話的最后一次修改時間,類型為一個datetime 對象。默認為當前的時間。expiry:會話的過期信息,類型為一個datetime 對象、一個整數(以秒為單位)或None。默認為通過set_expiry()保存在會話中的值,如果沒有則為None。get_expiry_date()
返回過期的日期。對于沒有自定義過期的會話(或者設置為瀏覽器關閉時過期的會話),它將等于從現(xiàn)在開始SESSION_COOKIE_AGE秒后的日期。
這個函數接受與get_expiry_age()一樣的關鍵字參數。
get_expire_at_browser_close()
返回True 或False,取決于用戶的會話Cookie在用戶瀏覽器關閉時會不會過期。
clear_expired()
從會話的存儲中清除過期的會話。這個類方法被clearsessions調用。
cycle_key()
創(chuàng)建一個新的會話,同時保留當前的會話數據。django.contrib.auth.login() 調用這個方法來減緩會話的固定。
在1.6 版以前,在保存會話數據到后端之前Django 默認使用pickle 來序列化它們。如果你使用的是簽名的Cookie 會話后端 并且SECRET_KEY 被攻擊者知道(Django 本身沒有漏洞會導致它被泄漏),攻擊者就可以在會話中插入一個字符串,在unpickle 之后可以在服務器上執(zhí)行任何代碼。在因特網上這個攻擊技術很簡單并很容易查到。盡管Cookie 會話的存儲對Cookie 保存的數據進行了簽名以防止篡改,SECRET_KEY 的泄漏會立即使得可以執(zhí)行遠端的代碼。
這種攻擊可以通過JSON而不是pickle序列化會話數據來減緩。為了幫助這個功能,Django 1.5.3 引入一個新的設置,SESSION_SERIALIZER,來自定義會話序列化的格式。為了向后兼容,這個設置在Django 1.5.x 中默認為django.contrib.sessions.serializers.PickleSerializer,但是為了增強安全性,在Django 1.6 中默認為django.contrib.sessions.serializers.JSONSerializer。即使在編寫你自己的序列化方法講述的說明中,我們也強烈建議依然使用JSON 序列化,特別是在你使用的是Cookie 后端時。
class serializers.JSONSerializer
對 django.core.signing中的JSON 序列化方法的一個包裝。只可以序列基本的數據類型。
另外,因為JSON 只支持字符串作為鍵,注意使用非字符串作為request.session 的鍵將不工作:
>>> # initial assignment
>>> request.session[0] = 'bar'
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0] # KeyError
>>> request.session['0']
'bar'
參見編寫你自己的序列化器 一節(jié)以獲得更多關于JSON 序列化的限制。
class serializers.PickleSerializer
支持任意Python 對象,但是正如上面描述的,可能導致遠端執(zhí)行代碼的漏洞,如果攻擊者知道了SECRET_KEY。
注意,與PickleSerializer不同,JSONSerializer 不可以處理任意的Python 數據類型。這是常見的情況,需要在便利性和安全性之間權衡。如果你希望在JSON 格式的會話中存儲更高級的數據類型比如datetime 和 Decimal,你需要編寫一個自定義的序列化器(或者在保存它們到request.session中之前轉換這些值到一個可JSON 序列化的對象)。雖然序列化這些值相當簡單直接 (django.core.serializers.json.DateTimeAwareJSONEncoder 可能幫得上忙),編寫一個解碼器來可靠地取出相同的內容卻能困難。例如,返回一個datetime 時,它可能實際上是與datetime 格式碰巧相同的一個字符串)。
你的序列化類必須實現(xiàn)兩個方法,dumps(self, obj) 和loads(self, data) 來分別序列化和去序列化會話數據的字典。
在request.session 上使用普通的Python 字符串作為字典的鍵。這主要是為了方便而不是一條必須遵守的規(guī)則。
以一個下劃線開始的會話字典的鍵被Django保留作為內部使用。
不要新的對象覆蓋request.session,且不要訪問或設置它的屬性。要像Python 字典一樣使用它。
下面這個簡單的視圖在一個用戶提交一個評論后設置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.")
...下面是登出一個成員的視圖,已經上面的login():
def logout(request):
try:
del request.session['member_id']
except KeyError:
pass
return HttpResponse("You're logged out.")
標準的django.contrib.auth.logout() 函數實際上所做的內容比這個要多一點以防止意外的數據泄露。它調用的request.session的flush()方法。我們使用這個例子來演示如何利用會話對象來工作,而不是一個完整的logout()實現(xiàn)。
為了方便,Django 提供一個簡單的方法來測試用戶的瀏覽器時候接受Cookie。只需在一個視圖中調用request.session的set_test_cookie()方法,并在接下來的視圖中調用test_cookie_worked() —— 不是在同一個視圖中調用。
由于Cookie的工作方式,在set_test_cookie() 和test_cookie_worked() 之間這種笨拙的分離是必要的。當你設置一個Cookie,直到瀏覽器的下一個請求你不可能真實知道一個瀏覽器是否接受了它。
使用delete_test_cookie() 來自己清除測試的Cookie是一個很好的實踐。請在你已經驗證測試的Cookie 已經工作后做這件事。
下面是一個典型的使用示例:
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_to_response('foo/login_form.html')
注
這一節(jié)中的示例直接從
django.contrib.sessions.backends.db中導入SessionStore對象。在你的代碼中,你應該從SESSION_ENGINE指定的會話引擎中導入SessionStore,如下所示:
>>> 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.save()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
>>> s['last_login']
1376587691
為了減緩會話固話攻擊,不存在的會話的鍵將重新生成:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore(session_key='no-such-session-here')
>>> s.save()
>>> s.session_key
'ff882814010ccbc3c870523934fee5a2'
如果你使用的是django.contrib.sessions.backends.db 后端,每個會話只是一個普通的Django 模型。Session 模型定義在 django/contrib/sessions/models.py中。因為它是一個普通的模型,你可以使用普通的Django 數據庫API 來訪問會話:
>>> 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 只有在會話被修改時才會保存會話到數據庫中 —— 即它的字典中的任何值被賦值或刪除時:
# Session is modified.
request.session['foo'] = 'bar'
# Session is modified.
del request.session['foo']
# Session is modified.
request.session['foo'] = {}
# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session['foo']['bar'] = 'baz'
上面例子的最后一種情況,我們可以通過設置會話對象的modified屬性顯式地告訴會話對象它已經被修改過:
request.session.modified = True
若要修改這個默認的行為,可以設置 SESSION_SAVE_EVERY_REQUEST 為True。當設置為True時,Django 將對每個請求保存會話到數據庫中。
注意會話的Cookie 只有在一個會話被創(chuàng)建或修改后才會發(fā)送。如果SESSION_SAVE_EVERY_REQUEST 為True,會話的Cookie 將在每個請求中發(fā)送。
類似地,會話Cookie 的expires 部分在每次發(fā)送會話Cookie 時更新。
如果響應的狀態(tài)碼時500,則會話不會被保存。
你可以通過SESSION_EXPIRE_AT_BROWSER_CLOSE設置來控制會話框架使用瀏覽器時長的會話,還是持久的會話。
默認情況下,SESSION_EXPIRE_AT_BROWSER_CLOSE設置為False,表示會話的Cookie 保存在用戶的瀏覽器中的時間為SESSION_COOKIE_AGE。如果你不想讓大家每次打開瀏覽器時都需要登錄時可以這樣使用。
如果SESSION_EXPIRE_AT_BROWSER_CLOSE 設置為True,Django 將使用瀏覽器時長的Cookie —— 用戶關閉他們的瀏覽器時立即過期。如果你想讓大家在每次打開瀏覽器時都需要登錄時可以這樣使用。
這個設置是一個全局的默認值,可以通過顯式地調request.session 的set_expiry() 方法來覆蓋,在上面的在視圖中使用會話中有描述。
注
某些瀏覽器(例如Chrome)提供一種設置,允許用戶在關閉并重新打開瀏覽器后繼續(xù)使用會話。在某些情況下,這可能干擾
SESSION_EXPIRE_AT_BROWSER_CLOSE設置并導致會話在瀏覽器關閉后不會過期。在測試啟用SESSION_EXPIRE_AT_BROWSER_CLOSE設置的Django 應用時請注意這點。
隨著用戶在你的網站上創(chuàng)建新的會話,會話數據可能會在你的會話存儲倉庫中積累。如果你正在使用數據庫作為后端,django_session 數據庫表將持續(xù)增長。如果你正在使用文件作為后端,你的臨時目錄包含的文件數量將持續(xù)增長。
要理解這個問題,考慮一下數據庫后端發(fā)生的情況。當一個用戶登入時,Django 添加一行到django_session 數據庫表中。每次會話數據更新時,Django 將更新這行。如果用戶手工登出,Django 將刪除這行。但是如果該用戶不登出,該行將永遠不會刪除。以文件為后端的過程類似。
Django 不提供自動清除過期會話的功能。因此,定期地清除會話是你的任務。Django 提供一個清除用的管理命令來滿足這個目的:clearsessions。建議定義調用這個命令,例如作為一個每天運行的Cron 任務。
注意,以緩存為后端不存在這個問題,因為緩存會自動刪除過期的數據。以cookie 為后端也不存在這個問題,因為會話數據通過用戶的瀏覽器保存。
一些Django 設置 讓你可以控制會話的行為:
一個站點下的子域名能夠在客戶端為整個域名設置Cookie。如果子域名不收信任的用戶控制且允許來自子域名的Cookie,那么可能發(fā)生會話固定。
例如,一個攻擊者可以登錄good.example.com并為他的賬號獲取一個合法的會話。如果該攻擊者具有bad.example.com的控制權,那么他可以使用這個域名來發(fā)送他的會話ID給你,因為子域名允許在*.example.com上設置Cookie。當你訪問good.example.com時,你將被登錄成攻擊者而沒有注意到并輸入你的敏感的個人信息(例如,信用卡信息)到攻擊者的賬號中。
另外一個可能的攻擊是,如果good.example.com設置它的 SESSION_COOKIE_DOMAIN 為".example.com" ,這將導致來自該站點的會話Cookie 被發(fā)送到bad.example.com。
JSONSerializer時,會話字典接收任何可json 序列化的值,當使用PickleSerializer時接收任何pickleable 的Python對象。更多信息參見pickle 模塊。django_session 的表中。Django 會話框架完全地、唯一地基于Cookie。它不像PHP一樣,實在沒辦法就把會話的ID放在URL 中。這是一個故意的設計。這個行為不僅使得URL變得丑陋,還使得你的網站易于受到通過"Referer" 頭部竊取會話ID的攻擊。
譯者:Django 文檔協(xié)作翻譯小組,原文:Sessions。
本文以 CC BY-NC-SA 3.0 協(xié)議發(fā)布,轉載請保留作者署名和文章出處。
Django 文檔協(xié)作翻譯小組人手緊缺,有興趣的朋友可以加入我們,完全公益性質。交流群:467338606。