《Web介面開發與自動化測試基於Python語言》–第3章
《Web介面開發與自動化測試基於Python語言》–讀書筆記
第3章
3.1 來寫個登入功能
修改/guest/sign/templates/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Django Page</title>
</head>
<body>
<h1>釋出會管理</h1>
<form >
<input name="username" type="text" placeholder="username"><br>
<input name="password" type="password" placeholder="password"><br>
<button id="btn" type="submit">登入</button>
</form>
</body>
</html>
思考如下問題:
當用戶輸入使用者名稱密碼並點選登入按鈕後,登入表單form中的資料以何種方式提交GET/POST到伺服器端?
Django如何驗證使用者名稱密碼的正確性?
如果驗證成功應該如何處理?
如果驗證失敗應該如何處理?
下面一一解答上述的疑問。
GET與POST請求
GET:從指定的資源請求資料;
POST:向指定的資源提交要被處理的資料。
GET請求
get方法傳遞引數,修改index.html,給form表單增加屬性method=”get”:
<form method="get">
<input name="username" type="text" placeholder="username"><br>
<input name="password" type="password" placeholder="password"><br>
<button id="btn" type="submit">登入</button>
</form>
輸入使用者名稱密碼admin/admin123,點選登入按鈕,瀏覽器的URL位址列變為:
http://127.0.0.1:8000/index/?username=admin&password=admin123
get方法會將使用者提交的資料新增到URL地址中,路徑後面跟問號?,其中username為HTML程式碼中<\input>標籤的name屬性值(name=”username”),admin是使用者在使用者名稱輸入框中填寫的使用者名稱,多個引數之間用&符號分隔。
POST請求
修改method=”post”,再次輸入使用者名稱密碼並點選登入按鈕,頁面提示403錯誤:
建議:仔細閱讀錯誤幫助資訊有助於解決問題。
CSRF漏洞:跨站請求偽造漏洞,Cross-Site Request Forgery。
Django針對CSRF的保護措施是在生成的每個表單中放置一個自動生成的令牌,通過這個令牌判斷POST請求是否來自同一個網站。
還是修改index.html,增加csrf資訊,使用Django模板中的標籤:{% csrf_token %}
再次重新整理頁面並使用瀏覽器的F12功能檢視POST請求具體資訊:
Form Data
username:admin
password:admin123
csrfmiddlewaretoken:CEueSfB9S4bJQoW9x……n0OvGDAhWC9rIH2S7PegwZew
可以看到當頁面向Django伺服器傳送POST請求的時候,伺服器端還要求客戶端加上csrfmiddlewaretoken欄位,該欄位的值為當前會話ID加上一個金鑰的雜湊值。
如果想忽略掉該檢查,可以修改/guest/settings.py檔案的MIDDLEWARE:
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',
]
處理登入請求
Django伺服器如何接收請求的資料並加以處理呢?
還是修改index.html,在form表單中增加action屬性來指定提交的路徑:
<form method="post" action="/login_action/">
這樣,當我們再次點選登入按鈕,將由http://127.0.0.1:8000/login_action/路徑來提交登入請求。
我們再來處理login_action,先修改/guest/urls.py檔案增加login_action的路由:
from sign import views
urlpatterns = [
url(r'^login_action/$', views.login_action)
]
然後修改檢視/guest/sign/views.py檔案,增加login_action函式來處理驗證資訊:
from django.http import HttpResponse
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, "index.html")
# 登入動作
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
if username == 'admin' and password == 'admin123':
return HttpResponse("Login Success!")
else:
return render(request, 'index.html', {'error':'username or password error!'})
對上述程式碼進行分析:
通過request.method得到客戶端的請求方式,並判斷是否為POST方式;
通過request.POST來獲取POST請求資料,通過.get()方法獲取username、password,如果引數為空,則返回一個空的字串;
通過if語句判斷username、password是否為admin/admin123,如果是則通過HttpResponse類返回字串Login Success!,否則通過render返回index.html登入頁面,並且同時返回錯誤提示的字典。
注意:
由於views.py裡的login_action函式定義了登入失敗時的錯誤提示,所以要同步修改index.html,增加錯誤資訊展示的程式碼,修改如下:
<input name="password" type="password" placeholder="password"><br>
{{ error }}<br>
<button id="btn" type="submit">登入</button>
這裡依然是使用了Django模板的標籤功能,它對應render返回字典中的key,即“error”,在登入失敗的頁面中顯示對應的value,即“username or password error!”。
重新整理/index/檢視登入成功和登入失敗的效果。
登入成功頁
當登入驗證通過後,就需要其他HTML頁面來展示下一步內容。
根據本例釋出會簽到系統,登入成功後應進入釋出會管理頁面,建立/guest/sign/templates/event_manage.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Event Manage Page</title>
</head>
<body>
<h1>Login Success!</h1>
</body>
</html>
同時修改檢視/guest/sign/views.py檔案增加event_manage函式:
from django.http import HttpResponse, HttpResponseRedirect
# 登入動作
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
if username == 'admin' and password == 'admin123':
#return HttpResponse("Login Success!")
return HttpResponseRedirect('/event_manage/')
else:
return render(request, 'index.html', {'error':'username or password error!'})
# 釋出會管理
def event_manage(request):
return render(request, "event_manage.html")
注意: HttpResponseRedirect類,它可以對路徑進行重定向,從而將登入成功之後的請求指向/event_mange/目錄,即http://127.0.0.1:8000/event_manage/。
最後修改/guest/urls.py檔案增加event_mange路由:
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^index/$', views.index),
url(r'^login_action/$', views.login_action),
url(r'^event_manage/$', views.event_manage),
]
3.2 Cookie和Session
通過在登入成功後,增加登入成功頁顯示“嘿,admin 你好!”,來研究Cookie和Session。
Cookie機制: Cookie分發通過擴充套件HTTP協議來實現,伺服器通過在HTTP的響應頭中加上一行特殊的指示來提示瀏覽器按照指示生成相應的Cookie。瀏覽器檢查所有儲存的Cookie,如果某個Cookie所宣告的作用範圍大於等於將要請求的資源所在的位置,則把該Cookie附在請求資源的HTTP請求頭上發給伺服器。
Session機制: Session機制是一種伺服器端的機制,伺服器使用一種類似於散列表的結構來儲存資訊。
Cookie的使用
這裡我調整了下順序,感覺按作者之前提到的Django的工作順序來講解程式碼的修改更合理:
首先,修改/guest/urls.py檔案,由於還是使用上面提到的登入成功後的展示頁面,所以依然使用event_mange路由,無需修改;
其次,修改/guest/sign/views.py檔案,新增Cookie處理相關程式碼:
# 登入動作
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
if username == 'admin' and password == 'admin123':
#return HttpResponse("Login Success!")
#return HttpResponseRedirect('/event_manage/')
response = HttpResponseRedirect('/event_manage/')
response.set_cookie('user', username, 3600) # 新增瀏覽器Cookie
return response
else:
return render(request, 'index.html', {'error':'username or password error!'})
# 釋出會管理
def event_manage(request):
username = request.COOKIES.get('user', '') # 讀取瀏覽器Cookie
return render(request, "event_manage.html", {"user":username})
講解上述程式碼,當用戶登入成功後,在跳轉到event_mange檢視函式的過程中,通過set_cookie()方法向瀏覽器中新增Cookie資訊。
set_cookie()方法傳遞了三個引數:user,用於表示寫入瀏覽器的Cookie名;username,是由使用者在登入頁面上輸入的使用者名稱即admin;3600,是用於設定Cookie資訊在瀏覽器中的保持時間,單位為秒。
在event_mange檢視函式中,通過request.COOKIES來讀取Cookie名為“user”的值,並且通過render將它和event_mange.html頁面一起返回。
- 最後,修改/guest/sign/templates/event_manage.html檔案,新增<\div>標籤來顯示使用者名稱:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Event Manage Page</title>
</head>
<div style="float:right;">
<a>嘿!{{ user }} 歡迎</a><hr/>
</div>
<body>
<h1>Login Success!</h1>
</body>
</html>
注意: 這裡仍使用模板標籤,user將會由views.py中的event_mange函式獲取的username所代替。
最後重新登入檢視效果,並且通過瀏覽器的F12功能檢視POST請求的頭部中Cookie資訊為:
Cookie:csrftoken=djsNd8BDErmFNZVXqg……9nZrDez5PgQFIaVLcVHhHcA1; user=admin
Session的使用
Cookie雖然好但是存在安全性問題,不法分子可能會篡改Cookie獲取想要的資料。
Session相比要安全很多,在Django中使用Session和Cookie類似,只需要將Cookie的幾步操作替換為Session操作即可。
只需要修改/guest/sign/views.py檔案:
# 登入動作
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
if username == 'admin' and password == 'admin123':
#return HttpResponse("Login Success!")
#return HttpResponseRedirect('/event_manage/')
response = HttpResponseRedirect('/event_manage/')
#response.set_cookie('user', username, 3600) # 新增瀏覽器Cookie
request.session['user'] = username # 將session資訊新增到瀏覽器
return response
else:
return render(request, 'index.html', {'error':'username or password error!'})
# 釋出會管理
def event_manage(request):
#username = request.COOKIES.get('user', '') # 讀取瀏覽器Cookie
username = reuqest.session.get('user', '') # 讀取瀏覽器session
return render(request, "event_manage.html", {"user":username})
再次重新整理頁面登入,得到了如下錯誤:
OperationalError at /login_action/
no such table: django_session
為什麼會有此錯誤?
原因:使用Session從Web伺服器來記錄使用者資訊,就應該有存放使用者sessionid對應資訊的地方才行,即需要在資料庫中建立用於存放sessionid的表。
Django已經準備好這些常用表,只需要使用如下命令來生成即可:
root@TEST:/home/test/guest# python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying sessions.0001_initial... OK
migrate命令可進行資料遷移,那麼問題來了,我們並沒有設定資料庫,為什麼已經生成了資料庫表呢?
這是因為Django預設設定了SQLite3資料庫,資料庫配置儲存在/guest/settings.py檔案中:
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
3.3 Django認證系統
前面的例子,對於使用者身份的驗證,只是使用了簡單的if語句進行判斷,本節會實現真正的使用者資訊驗證。
登入Admin後臺
在使用migrate命令進行資料遷移時,Django同時也建立了auth_user表,該表中存放的使用者資訊可以用來登入Django自帶的Admin管理後臺。在此之前,需要使用如下命令,建立Admin帳號:
python manage.py createsuperuser
本例建立的超級管理員Admin帳號:admin/admin123456
都是圖形操作介面,不再做過多解釋。可自己動手嘗試增加使用者。
引用Django認證登入
Django已經封裝好了使用者認證和登入的相關方法,只需要拿來用即可。並且,同樣使用auth_user表中的資料進行驗證,前面已經通過Admin後臺向該表中添加了使用者資訊。
要達到此目的,只需要修改/guest/sign/views.py檔案中的login_action函式:
from django.contrib import auth
# 登入動作
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
user = auth.authenticate(username=username, password=password)
if user is not None:
auth.login(request, user) # 登入
#if username == 'admin' and password == 'admin123':
#return HttpResponse("Login Success!")
#return HttpResponseRedirect('/event_manage/')
#response.set_cookie('user', username, 3600) # 新增瀏覽器Cookie
request.session['user'] = username # 將session資訊新增到瀏覽器
response = HttpResponseRedirect('/event_manage/')
return response
else:
return render(request, 'index.html', {'error':'username or password error!'})
程式碼講解:
authenticate()函式認證給出的使用者名稱和密碼,它接受兩個引數:username、password,並且會在使用者名稱密碼正確的情況下返回一個user物件,否則authenticate()返回None。
if語句對authenticate()返回物件進行判斷,如果不為None,說明使用者認證通過,則呼叫auth的login()函式進行登入,它接受兩個引數:HttpRequest物件、一個user物件。
返回到index頁面,嘗試使用admin/admin123456和自定義使用者進行登入。
關上窗戶
不需要認證也可以訪問這個頁面,這是個漏洞,我們不允許使用者不經認證就直接訪問這些頁面,所有需要將這個窗戶關閉。
要做到這點,只需要再指定的檢視函式前增加裝飾器即可,如下修改/guest/sign/views.py檔案:
from django.contrib.auth.decorators import login_required
# 釋出會管理
@login_required
def event_manage(request):
#username = request.COOKIES.get('user', '') # 讀取瀏覽器Cookie
username = request.session.get('user', '') # 讀取瀏覽器Session
return render(request, "event_manage.html", {"user":username})
清空快取,再次訪問/event_manage/,Django會提示頁面不存在:Page not found 404
我們可以發現一個問題,當訪問@login_required裝飾器的檢視時,預設跳轉的URL中會包含“/accounts/login/”,這裡為什麼不讓它直接跳轉到登入頁面?不僅要告訴使用者窗戶是關著的,還指引使用者到門的位置來進行登入操作呢?
想要達到這個目的,需要做如下修改,在/guest/urls.py檔案中,增加新的路徑配置:
from sign import views
urlpatterns = [
url(r'^$', views.index),
url(r'^admin/', admin.site.urls),
#url(r'^index/$', views.index),
url(r'^login_action/$', views.login_action),
url(r'^event_manage/$', views.event_manage),
url(r'^accounts/login/$', views.index),
]