我們將建立一個用戶注冊和身份驗證系統,讓用戶能夠注冊賬戶,進而登錄和注銷。我們將創建一個新的應用程序,其中包含與處理用戶賬戶相關的所有功能。我們還將對模型Topic 稍做修改,讓每個主題都歸屬於特定用戶。
1、應用程序users
我們首先使用命令startapp 來創建一個名為users 的應用程序:
(ll_env)learning_log$ python manage.py startapp users (ll_env)learning_log$ ls db.sqlite3 learning_log learning_logs ll_env manage.py users (ll_env)learning_log$ ls users admin.py __init__.py migrations models.py tests.py views.py
這個命令新建一個名為users的目錄,其結構與應用程序learning_logs 相同。
1.1 將應用程序users 添加到settings.py中
在settings.py中,我們需要將這個新的應用程序添加到INSTALLED_APPS 中,如下所示:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 我的應用程序 'learning_logs', 'users', ]
1.2 包含應用程序users 的URL
接下來,我們需要修改項目根目錄中的urls.py,使其包含我們將為應用程序users 定義的URL:
from django.contrib import admin from django.conf.urls import url, include urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'', include('learning_logs.urls',namespace='learning_logs')), url(r'^users/', include('users.urls', namespace='users')), ]
我們添加了一行代碼,以包含應用程序users 中的文件urls.py。這行代碼與任何以單詞users打頭的URL(如http://localhost:8000/users/login/)都匹配。我們還創建了命名空 間'users' ,以便將應用程序learning_logs 的URL同應用程序users 的URL區分開來。
2、登陸頁面
我們首先來實現登錄頁面的功能。為此,我們將使用Django提供的默認登錄視圖,因此URL模式會稍有不同。在目錄learning_log/users/中,新建一個名為urls.py的文件,並在其中添加如下代碼:
from django.conf.urls import url from django.contrib.auth.views import LoginView from . import views app_name='users' urlpatterns = [ # 登錄頁面 url(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'), ]
我們首先導入了默認視圖login 。登錄頁面的URL模式與URL http://localhost:8000/users/login/匹配。這個URL中的單詞users讓Django在users/urls.py中查找,而單詞 login讓它將請求發送給Django默認視圖login (請注意,視圖實參為login ,而不是views.login )。鑒於我們沒有編寫自己的視圖函數,我們傳遞了一個字典,告訴Django 去哪里查找我們將編寫的模板。這個模板包含在應用程序users 而不是learning_logs 中。
(1)模板login.html
用戶請求登錄頁面時,Django將使用其默認視圖login ,但我們依然需要為這個頁面提供模板。為此,在目錄learning_log/users/中,創建一個名為templates的目錄,並在其中創建一個名為users的目錄。以下是模板login.html,你應將其存儲到目錄learning_log/users/templates/users/中:
{% extends "learning_logs/base.html" %} {% block content %} {% if form.errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} <form method="post" action="{% url 'users:login' %}"> {% csrf_token %} {{ form.as_p }} <button name="submit">log in</button> <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" /> </form> {% endblock content %}
- 這個模板繼承了base.html,旨在確保登錄頁面的外觀與網站的其他頁面相同。請注意,一個應用程序中的模板可繼承另一個應用程序中的模板。
- 如果表單的errors 屬性被設置,我們就顯示一條錯誤消息,指出輸入的用戶名—密碼對與數據庫中存儲的任何用戶名—密碼對都不匹配。
我們要讓登錄視圖處理表單,因此將實參action 設置為登錄頁面的URL。登錄視圖將一個表單發送給模板,在模板中,我們顯示這個表單並添加一個提交按 鈕。我們包含了一個隱藏的表單元素——'next' ,其中的實參value 告訴Django在用戶成功登錄后將其重定向到什么地方——在這里是主頁。
(2)鏈接到登陸頁面
下面在learning_logs/templates/learning_logs/base.html中添加到登錄頁面的鏈接,讓所有頁面都包含它。用戶已登錄時,我們不想顯示這個鏈接,因此將它嵌套在一個{% if %}標簽中:
<p> <a href="{% url 'learning_logs:index' %}">Learning Log</a> <a href="{% url 'learning_logs:topics' %}">Topics</a> {% if user.is_authenticated %} Hello, {{ user.username }}. {% else %} <a href="{% url 'users:login' %}">log in</a> {% endif %} </p> {% block content %}{% endblock content %}
在Django身份驗證系統中,每個模板都可使用變量user ,這個變量有一個is_authenticated 屬性:如果用戶已登錄,該屬性將為True ,否則為False 。這讓你能夠向已 通過身份驗證的用戶顯示一條消息,而向未通過身份驗證的用戶顯示另一條消息。
在這里,我們向已登錄的用戶顯示一條問候語。對於已通過身份驗證的用戶,還設置了屬性username ,我們使用這個屬性來個性化問候語,讓用戶知道他已登錄。對於還未通過身份驗證的用戶,我們再顯示一個到登錄頁面的鏈接。
(3)使用登陸頁面
前面建立了一個用戶賬戶,下面來登錄一下,看看登錄頁面是否管用。請訪問http://localhost:8000/admin/,如果你依然是以管理員的身份登錄的,請在頁眉上找到注銷鏈接並單擊
它。
注銷后,訪問http://localhost:8000/users/login/,你將看到類似於圖19-4所示的登錄頁面。輸入你在前面設置的用戶名和密碼,將進入頁面index。。在這個主頁的頁眉中,顯示了一條 個性化問候語,其中包含你的用戶名。
3、注銷
現在需要提供一個讓用戶注銷的途徑。我們不創建用於注銷的頁面,而讓用戶只需單擊一個鏈接就能注銷並返回到主頁。為此,我們將為注銷鏈接定義一個URL模式,編寫一個 視圖函數,並在base.html中添加一個注銷鏈接。
(1)注銷url
下面的代碼為注銷定義了URL模式,該模式與URL http://locallwst:8000/users/logout/匹配。修改后的users/urls.py如下:
from django.conf.urls import url from django.contrib.auth.views import LoginView from . import views app_name='users' urlpatterns = [ # 登錄頁面 url(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'), # 注銷 url(r'^logout/$', views.logout_view, name='logout'), ]
這個URL模式將請求發送給函數logout_view() 。這樣給這個函數命名,旨在將其與我們將在其中調用的函數logout() 區分開來。
2. 視圖函數logout_view()
函數logout_view() 很簡單:只是導入Django函數logout() ,並調用它,再重定向到主頁。請打開users/views.py,並輸入下面的代碼:
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.contrib.auth import logout def logout_view(request): """注銷用戶""" logout(request) return HttpResponseRedirect(reverse('learning_logs:index'))
我們從django.contrib.auth中導入了函數logout()。我們調用了函數logout() ,它要求將request 對象作為實參。然后,我們重定向到主頁。
(3)鏈接到注銷頁面
現在我們需要添加一個注銷鏈接。我們在learning_logs/templates/learning_logs/base.html 中添加這種鏈接,讓每個頁面都包含它;我們將它放在標簽{% if user.is_authenticated %}中,使得僅當用戶登錄后 才能看到它:
<p> <a href="{% url 'learning_logs:index' %}">Learning Log</a> <a href="{% url 'learning_logs:topics' %}">Topics</a> {% if user.is_authenticated %} Hello, {{ user.username }}. <a href="{% url 'users:logout' %}">log out</a> {% else %} <a href="{% url 'users:login' %}">log in</a> {% endif %} </p> {% block content %}{% endblock content %}
下圖顯示了用戶登錄后看到的主頁。這里的重點是創建能夠正確工作的網站,因此幾乎沒有設置任何樣式。確定所需的功能都能正確運行后,我們將設置這個網站的樣式,使 其看起來更專業。
4、注冊頁面
下面來創建一個讓新用戶能夠注冊的頁面。我們將使用Django提供的表單UserCreationForm,但編寫自己的視圖函數和模板。
(1)注冊頁面的URL模式
下面的代碼定義了注冊頁面的URL模式,它也包含在users/urls.py中:
from django.conf.urls import url from django.contrib.auth.views import LoginView from . import views app_name='users' urlpatterns = [ # 登錄頁面 url(r'^login/$', LoginView.as_view(template_name='users/login.html'), name='login'), # 注銷 url(r'^logout/$', views.logout_view, name='logout'), # 注冊頁面 url(r'^register/$', views.register, name='register'), ]
(2). 視圖函數register()
在注冊頁面首次被請求時,視圖函數register() 需要顯示一個空的注冊表單,並在用戶提交填寫好的注冊表單時對其進行處理。如果注冊成功,這個函數還需讓用戶自動登 錄。請在users/views.py中添加如下代碼:
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.contrib.auth import login, logout, authenticate from django.contrib.auth.forms import UserCreationForm def logout_view(request): """注銷用戶""" logout(request) return HttpResponseRedirect(reverse('learning_logs:index')) def register(request): """注冊新用戶""" if request.method != 'POST': # 顯示空的注冊表單 form = UserCreationForm() else: # 處理填寫好的表單 form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() # 讓用戶自動登錄,再重定向到主頁 authenticated_user = authenticate(username=new_user.username, password=request.POST['password1']) login(request, authenticated_user) return HttpResponseRedirect(reverse('learning_logs:index')) context = {'form': form} return render(request, 'users/register.html', context)
- 我們首先導入了函數render() ,然后導入了函數login() 和authenticate() ,以便在用戶正確地填寫了注冊信息時讓其自動登錄。我們還導入了默認表 單UserCreationForm 。
- 在函數register() 中,我們檢查要響應的是否是POST請求。如果不是,就創建一個UserCreationForm 實例,且不給它提供任何初始數據。如果響應的是POST請求,我們就根據提交的數據創建一個UserCreationForm 實例,並檢查這些數據是否有效:就這里而言,是用戶名未包含非法字符,輸入的兩個密碼相同,以及用戶沒有試圖做惡意的事情。
- 如果提交的數據有效,我們就調用表單的方法save() ,將用戶名和密碼的散列值保存到數據庫中。方法save() 返回新創建的用戶對象,我們將其存儲在new_user 中。
- 保存用戶的信息后,我們讓用戶自動登錄,這包含兩個步驟。首先,我們調用authenticate() ,並將實參new_user.username 和密碼傳遞給它。用戶注冊時, 被要求輸入密碼兩次;由於表單是有效的,我們知道輸入的這兩個密碼是相同的,因此可以使用其中任何一個。在這里,我們從表單的POST數據中獲取與鍵'password1' 相關 聯的值。如果用戶名和密碼無誤,方法authenticate() 將返回一個通過了身份驗證的用戶對象,而我們將其存儲在authenticated_user 中。
- 我們調用函 數login() ,並將對象request 和authenticated_user 傳遞給它,這將為新用戶創建有效的會話。最后,我們將用戶重定向到主頁,其頁眉中顯示了 一條個性化的問候語,讓用戶知道注冊成功了。
(3)注冊模板
注冊頁面的模板與登錄頁面的模板類似,請務必將其保存到login.html所在的目錄中:
{% extends "learning_logs/base.html" %} {% block content %} <form method="post" action="{% url 'users:register' %}"> {% csrf_token %} {{ form.as_p }} <button name="submit">register</button> <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" /> </form> {% endblock content %}
這里也使用了方法as_p ,讓Django在表單中正確地顯示所有的字段,包括錯誤消息——如果用戶沒有正確地填寫表單。
(4)鏈接到注冊頁面
我們在learning_logs/templates/learning_logs/base.html添加這樣的代碼,即在用戶沒有登錄時顯示到注冊頁面的鏈接:
<p> <a href="{% url 'learning_logs:index' %}">Learning Log</a> <a href="{% url 'learning_logs:topics' %}">Topics</a> {% if user.is_authenticated %} Hello, {{ user.username }}. <a href="{% url 'users:logout' %}">log out</a> {% else %} <a href="{% url 'users:register' %}">register</a> <a href="{% url 'users:login' %}">log in</a> {% endif %} </p> {% block content %}{% endblock content %}
現在,已登錄的用戶看到的是個性化的問候語和注銷鏈接,而未登錄的用戶看到的是注冊鏈接和登錄鏈接。請嘗試使用注冊頁面創建幾個用戶名各不相同的用戶賬戶。
注意 這里的注冊系統允許用戶創建任意數量的賬戶。有些系統要求用戶確認其身份:發送一封確認郵件,用戶回復后其賬戶才生效。通過這樣做,系統生成的垃圾賬戶將比這里使用的簡單系統少。然而,學習創建應用程序時,完全可以像這里所做的那樣,使用簡單的用戶注冊系統。
5、讓用戶擁有自己的數據
用戶應該能夠輸入其專有的數據,因此我們將創建一個系統,確定各項數據所屬的用戶,再限制對頁面的訪問,讓用戶只能使用自己的數據。
在本節中,我們將修改模型Topic ,讓每個主題都歸屬於特定用戶。這也將影響條目,因為每個條目都屬於特定的主題。我們先來限制對一些頁面的訪問。
5.1、使用@login_required 限制訪問
Django提供了裝飾器@login_required ,讓你能夠輕松地實現這樣的目標:對於某些頁面,只允許已登錄的用戶訪問它們。裝飾器 (decorator)是放在函數定義前面的指 令,Python在函數運行前,根據它來修改函數代碼的行為。下面來看一個示例。
1. 限制對topics 頁面的訪問
每個主題都歸特定用戶所有,因此應只允許已登錄的用戶請求topics 頁面。為此,在learning_logs/views.py中添加如下代碼: views.py
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.contrib.auth.decorators import login_required from .models import Topic, Entry from .forms import TopicForm,EntryForm def index(request): """學習筆記的主頁""" return render(request, 'learning_logs/index.html') @login_required def topics(request): """顯示所有的主題""" topics = Topic.objects.order_by('date_added') context = {'topics': topics} return render(request, 'learning_logs/topics.html', context) def topic(request, topic_id): """顯示特定主題的詳細頁面""" topic = Topic.objects.get(id=topic_id) entries = topic.entry_set.order_by('-date_added') context = {'topic': topic, 'entries': entries} return render(request, 'learning_logs/topic.html', context) def new_topic(request): """添加新主題""" if request.method != 'POST': # 未提交數據:創建一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topics')) context = {'form': form} return render(request, 'learning_logs/new_topic.html', context) def new_entry(request, topic_id): """在特定的主題中添加新條目""" topic = Topic.objects.get(id=topic_id) if request.method != 'POST': # 未提交數據,創建一個空表單 form = EntryForm() else: # POST提交的數據,對數據進行處理 form = EntryForm(data=request.POST) if form.is_valid(): new_entry = form.save(commit=False) new_entry.topic = topic new_entry.save() return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id])) context = {'topic': topic, 'form': form} return render(request, 'learning_logs/new_entry.html', context) def edit_entry(request, entry_id): """編輯既有條目""" entry = Entry.objects.get(id=entry_id) topic = entry.topic if request.method != 'POST': # 初次請求,使用當前條目填充表單 form = EntryForm(instance=entry) else: # POST提交的數據,對數據進行處理 form = EntryForm(instance=entry, data=request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id])) context = {'entry': entry, 'topic': topic, 'form': form} return render(request, 'learning_logs/edit_entry.html', context)
我們首先導入了函數login_required() 。我們將login_required() 作為裝飾器用於視圖函數topics() ——在它前面加上符號@ 和login_required ,讓Python在運 行topics() 的代碼前先運行login_required() 的代碼。
login_required() 的代碼檢查用戶是否已登錄,僅當用戶已登錄時,Django才運行topics() 的代碼。如果用戶未登錄,就重定向到登錄頁面。 為實現這種重定向,我們需要修改settings.py,讓Django知道到哪里去查找登錄頁面。請在項目learning_log的Django設置settings.py末尾添加如下代碼:
# 我的設置 LOGIN_URL = '/users/login/'
現在,如果未登錄的用戶請求裝飾器@login_required 的保護頁面,Django將重定向到settings.py中的LOGIN_URL 指定的URL。
要測試這個設置,可注銷並進入主頁。然后,單擊鏈接Topics,這將重定向到登錄頁面。接下來,使用你的賬戶登錄,並再次單擊主頁中的Topics鏈接,你將看到topics頁面。
2、全面限制對項目“學習筆記”的訪問
Django讓你能夠輕松地限制對頁面的訪問,但你必須針對要保護哪些頁面做出決定。最好先確定項目的哪些頁面不需要保護,再限制對其他所有頁面的訪問。你可以輕松地修改 過於嚴格的訪問限制,其風險比不限制對敏感頁面的訪問更低。
在項目“學習筆記”中,我們將不限制對主頁、注冊頁面和注銷頁面的訪問,並限制對其他所有頁面的訪問。 在下面的learning_logs/views.py中,對除index() 外的每個視圖都應用了裝飾器@login_required :
--snip-- @login_required def topics(request): --snip-- @login_required def topic(request, topic_id): --snip-- @login_required def new_topic(request): --snip-- @login_required def new_entry(request, topic_id): --snip-- @login_required def edit_entry(request, entry_id): --snip--
如果你在未登錄的情況下嘗試訪問這些頁面,將被重定向到登錄頁面。另外,你還不能單擊到new_topic 等頁面的鏈接。但如果你輸入URL http://localhost:8000/new_topic/,將重 定向到登錄頁面。對於所有與私有用戶數據相關的URL,都應限制對它們的訪問。
3、將數據關聯到用戶
現在,需要將數據關聯到提交它們的用戶。我們只需將最高層的數據關聯到用戶,這樣更低層的數據將自動關聯到用戶。例如,在項目“學習筆記”中,應用程序的最高層數據是
主題,而所有條目都與特定主題相關聯。只要每個主題都歸屬於特定用戶,我們就能確定數據庫中每個條目的所有者。
下面來修改模型Topic ,在其中添加一個關聯到用戶的外鍵。這樣做后,我們必須對數據庫進行遷移。最后,我們必須對有些視圖進行修改,使其只顯示與當前登錄的用戶相關 聯的數據。
(1)修改模型Topic
對models.py的修改只涉及兩行代碼:
from django.db import models from django.contrib.auth.models import User class Topic(models.Model): """用戶要學習的主題""" text = models.CharField(max_length=200) date_added = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User) def __str__(self): """返回模型的字符串表示""" return self.text class Entry(models.Model): --snip--
我們首先導入了django.contrib.auth 中的模型User ,然后在Topic 中添加了字段owner ,它建立到模型User 的外鍵關系。
(2) 確定當前有哪些用戶
我們遷移數據庫時,Django將對數據庫進行修改,使其能夠存儲主題和用戶之間的關聯。為執行遷移,Django需要知道該將各個既有主題關聯到哪個用戶。最簡單的辦法是,將既 有主題都關聯到同一個用戶,如超級用戶。為此,我們需要知道該用戶的ID。
下面來查看已創建的所有用戶的ID。為此,啟動一個Django shell會話,並執行如下命令:
(ll_env) [root@xxjt learning_log]# python3 manage.py shell>>> from django.contrib.auth.models import User >>> User.objects.all() <QuerySet [<User: happy>, <User: test1234>]> >>> >>> for user in User.objects.all(): ... print(user.username, user.id) ... happy 1 test1234 2
我們在shell會話中導入了模型User 。然后,我們查看到目前為止都創建了哪些用戶。 我們遍歷用戶列表,並打印每位用戶的用戶名和ID。Django詢問要將既有主題關聯到哪個用戶時,我們將指定其中的一個ID值。
3. 遷移數據庫
知道用戶ID后,就可以遷移數據庫了。
參考資料:
1、python編程,從入門到實踐