1. 程式人生 > >《Web介面開發與自動化測試基於Python語言》–第3章

《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!'})

對上述程式碼進行分析:

  1. 通過request.method得到客戶端的請求方式,並判斷是否為POST方式;

  2. 通過request.POST來獲取POST請求資料,通過.get()方法獲取username、password,如果引數為空,則返回一個空的字串;

  3. 通過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),
]