因為因特網HTTP協議的特性,每一次來自於用戶瀏覽器的請求(request)都是無狀態的、獨立的。
通俗地說,就是無法保存用戶狀態,后台服務器根本就不知道當前請求和以前及以后請求是否來自同一用戶。對於靜態網站,這可能不是個問題,而對於動態網站,尤其是京東、天貓、銀行等購物或金融網站,無法識別用戶並保持用戶狀態是致命的,根本就無法提供服務。
你可以嘗試將瀏覽器的cookie功能關閉,你會發現將無法在京東登錄和購物。
為了實現連接狀態的保持功能,網站會通過用戶的瀏覽器在用戶機器內被限定的硬盤位置中寫入一些數據,也就是所謂的Cookie。通過Cookie可以保存一些諸如用戶名、瀏覽記錄、表單記錄、登錄和注銷等各種數據。
但是這種方式非常不安全,因為Cookie保存在用戶的機器上,如果Cookie被偽造、篡改或刪除,就會造成極大的安全威脅,因此,現代網站設計通常將Cookie用來保存一些不重要的內容,實際的用戶數據和狀態還是以Session會話的方式保存在服務器端。
Session依賴Cookie!但與Cookie不同的地方在於Session將所有的數據都放在服務器端,用戶瀏覽器的Cookie中只會保存一個非明文的識別信息,比如哈希值。
Django提供了一個通用的Session框架,並且可以使用多種session數據的保存方式:
- 保存在數據庫內
- 保存到緩存
- 保存到文件內
- 保存到cookie內
通常情況,沒有特別需求的話,請使用保存在數據庫內的方式,盡量不要保存到Cookie內。
Django的session框架默認啟用,並已經注冊在app設置內,如果真的沒有啟用,那么參考下面的內容添加有說明的那兩行,再執行migrate命令創建數據表,就可以使用session了。
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', # 這一行 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # 這一行 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
當session啟用后,傳遞給視圖request參數的HttpRequest對象將包含一個session屬性,就像一個字典對象一樣。
你可以在Django的任何地方讀寫request.session
屬性,或者多次編輯使用它。
下面是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() 會調用這個方法。
基本上背下來上面的內容,Django的session你就可以信手拈來了。
一、使用session
下面結合我們的項目實戰,使用session。
首先,修改login/views.py
中的login()視圖函數:
def login(request): if request.session.get('is_login',None): return redirect("/index/") if request.method == "POST": login_form = forms.UserForm(request.POST) message = "請檢查填寫的內容!" if login_form.is_valid(): username = login_form.cleaned_data['username'] password = login_form.cleaned_data['password'] try: user = models.User.objects.get(name=username) if user.password == password: request.session['is_login'] = True request.session['user_id'] = user.id request.session['user_name'] = user.name return redirect('/index/') else: message = "密碼不正確!" except: message = "用戶不存在!"
login_form = forms.UserForm()
return render(request, 'login/login.html', locals())
通過下面的if語句,我們不允許重復登錄:
if request.session.get('is_login',None): return redirect("/index/")
通過下面的語句,我們往session字典內寫入用戶狀態和數據:
request.session['is_login'] = True request.session['user_id'] = user.id request.session['user_name'] = user.name
你完全可以往里面寫任何數據,不僅僅限於用戶相關!
既然有了session記錄用戶登錄狀態,那么就可以完善我們的登出視圖函數了:
def logout(request): if not request.session.get('is_login', None): # 如果本來就未登錄,也就沒有登出一說 return redirect("/index/") request.session.flush() # 或者使用下面的方法 # del request.session['is_login'] # del request.session['user_id'] # del request.session['user_name'] return redirect("/index/")
flush()方法是比較安全的一種做法,而且一次性將session中的所有內容全部清空,確保不留后患。但也有不好的地方,那就是如果你在session中夾帶了一點‘私貨’,會被一並刪除,這一點一定要注意。
二、完善頁面
有了用戶狀態,就可以根據用戶登錄與否,展示不同的頁面,比如導航條內容:
首先,修改base.html
文件:
{% load staticfiles %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3個meta標簽*必須*放在最前面,任何其他內容都*必須*跟隨其后! --> <title>{% block title %}base{% endblock %}</title> <!-- Bootstrap --> <link href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}" rel="stylesheet"> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script> <![endif]--> {% block css %}{% endblock %} </head> <body> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav" aria-expanded="false"> <span class="sr-only">切換導航條</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Mysite</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="my-nav"> <ul class="nav navbar-nav"> <li class="active"><a href="/index/">主頁</a></li> </ul> <ul class="nav navbar-nav navbar-right"> {% if request.session.is_login %} <li><a href="#">當前在線:{{ request.session.user_name }}</a></li> <li><a href="/logout/">登出</a></li> {% else %} <li><a href="/login/">登錄</a></li> <li><a href="/register/">注冊</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> {% block content %}{% endblock %} <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="{% static 'js/jquery-3.2.1.js' %}"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> </body> </html>
通過if判斷,當登錄時,顯示當前用戶名和登出按鈕。未登錄時,顯示登錄和注冊按鈕。
注意其中的模板語言,{{ request }}
這個變量會被默認傳入模板中,可以通過圓點的調用方式,獲取它內部的{{ request.session }}
,再進一步的獲取session中的內容。其實{{ request }}
中的數據遠不止此,例如{{ request.path }}
就可以獲取先前的url地址。
再修改一下index.html
頁面,根據登錄與否的不同,顯示不同的內容:
{% extends 'base.html' %} {% block title %}主頁{% endblock %} {% block content %} {% if request.session.is_login %} <h1>你好,{{ request.session.user_name }}!歡迎回來!</h1> {% else %} <h1>你尚未登錄,只能訪問公開內容!</h1> {% endif %} {% endblock %}
三、效果展示
這是用戶‘lisi’登錄后的界面:
這是未登錄用戶的界面:
四、創建注冊forms
顯而易見,我們的注冊頁面也需要一個form表單。同樣地,在/login/forms.py
中添加一個新的表單類:
class RegisterForm(forms.Form): gender = ( ('male', "男"), ('female', "女"), ) username = forms.CharField(label="用戶名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'})) password1 = forms.CharField(label="密碼", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) password2 = forms.CharField(label="確認密碼", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'})) email = forms.EmailField(label="郵箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'})) sex = forms.ChoiceField(label='性別', choices=gender) captcha = CaptchaField(label='驗證碼')
說明:
- gender字典和User模型中的一樣,其實可以拉出來作為常量共用,為了直觀,特意重寫一遍;
- password1和password2,用於輸入兩遍密碼,並進行比較,防止誤輸密碼;
- email是一個郵箱輸入框;
- sex是一個select下拉框;
五、完善register.html
同樣地,類似login.html文件,我們手工在register.html中編寫forms相關條目:
{% extends 'base.html' %} {% block title %}注冊{% endblock %} {% block content %} <div class="container"> <div class="col-md-4 col-md-offset-4"> <form class='form-register' action="/register/" method="post"> {% if message %} <div class="alert alert-warning">{{ message }}</div> {% endif %} {% csrf_token %} <h2 class="text-center">歡迎注冊</h2> <div class="form-group"> {{ register_form.username.label_tag }} {{ register_form.username}} </div> <div class="form-group"> {{ register_form.password1.label_tag }} {{ register_form.password1 }} </div> <div class="form-group"> {{ register_form.password2.label_tag }} {{ register_form.password2 }} </div> <div class="form-group"> {{ register_form.email.label_tag }} {{ register_form.email }} </div> <div class="form-group"> {{ register_form.sex.label_tag }} {{ register_form.sex }} </div> <div class="form-group"> {{ register_form.captcha.errors }} {{ register_form.captcha.label_tag }} {{ register_form.captcha }} </div> <button type="reset" class="btn btn-default pull-left">重置</button> <button type="submit" class="btn btn-primary pull-right">提交</button> </form> </div> </div> <!-- /container --> {% endblock %}
需要注意的是form標簽的action地址為/register/
。
六、實現注冊視圖
進入/login/views.py
文件,現在來完善我們的register()
視圖:
def register(request): if request.session.get('is_login', None): # 登錄狀態不允許注冊。你可以修改這條原則! return redirect("/index/") if request.method == "POST": register_form = forms.RegisterForm(request.POST) message = "請檢查填寫的內容!" if register_form.is_valid(): # 獲取數據 username = register_form.cleaned_data['username'] password1 = register_form.cleaned_data['password1'] password2 = register_form.cleaned_data['password2'] email = register_form.cleaned_data['email'] sex = register_form.cleaned_data['sex'] if password1 != password2: # 判斷兩次密碼是否相同 message = "兩次輸入的密碼不同!" return render(request, 'login/register.html', locals()) else: same_name_user = models.User.objects.filter(name=username) if same_name_user: # 用戶名唯一 message = '用戶已經存在,請重新選擇用戶名!' return render(request, 'login/register.html', locals()) same_email_user = models.User.objects.filter(email=email) if same_email_user: # 郵箱地址唯一 message = '該郵箱地址已被注冊,請使用別的郵箱!' return render(request, 'login/register.html', locals()) # 當一切都OK的情況下,創建新用戶 new_user = models.User() new_user.name = username new_user.password = password1 new_user.email = email new_user.sex = sex new_user.save() return redirect('/login/') # 自動跳轉到登錄頁面 register_form = forms.RegisterForm() return render(request, 'login/register.html', locals())
從大體邏輯上,也是先實例化一個RegisterForm的對象,然后使用is_valide()
驗證數據,再從cleaned_data
中獲取數據。
重點在於注冊邏輯,首先兩次輸入的密碼必須相同,其次不能存在相同用戶名和郵箱,最后如果條件都滿足,利用ORM的API,創建一個用戶實例,然后保存到數據庫內。
對於注冊的邏輯,不同的生產環境有不同的要求,請跟進實際情況自行完善,這里只是一個基本的注冊過程,不能生搬照抄。
讓我們看一下注冊的頁面:
頁面自動跳轉到登錄頁面。我們用新建的用戶登錄,發現OK。
七、密碼加密
等等!我們好像忘了什么!我們到現在都還一直在用明文的密碼!
對於如何加密密碼,有很多不同的途徑,其安全程度也高低不等。這里我們使用Python內置的hashlib庫,使用哈希值的方式加密密碼,可能安全等級不夠高,但足夠簡單,方便使用,不是么?
首先在login/views.py
中編寫一個hash函數:
import hashlib def hash_code(s, salt='mysite'):# 加點鹽 h = hashlib.sha256() s += salt h.update(s.encode()) # update方法只接收bytes類型 return h.hexdigest()
使用了sha256算法,加了點鹽。
然后,我們還要對login()和register()視圖進行一下修改:
注意其中關於密碼處理的部分!
好了,我們可以來驗證一下了!但是,請先在admin后台里,把我們前面創建的測試用戶全部刪除!因為它們的密碼沒有使用哈希算法加密,已經無效了。
重啟服務器,進入注冊頁面,新建一個用戶,然后進入admin后台,查看用戶的密碼情況:
可以看到密碼長度根據你哈希算法的不同,已經變得很長了,所以前面model中設置password字段時,不要想當然的將max_length
設置為16這么小的數字。