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