Python項目 | Web應用程序之用戶賬戶

用戶賬戶

Web應用程序的核心是讓任何用戶都能夠註冊賬戶並能夠使用它。在本章中,創建一些表單,讓用戶能夠添加主題和條目,以及編輯既有的條目。還將學習Django如何防範對基於表單的網頁發起的常見攻擊,

目標:實現一個用戶身份驗證系統。創建一個註冊頁面,供用戶創建賬戶,並讓有些頁面只能供已登錄的用戶訪問。接下來,修改一些視圖函數, 使得用戶只能看到自己的數據,以及確保用戶數據的安全。

1、讓用戶能夠輸入數據

【1】添加新主題

讓用戶輸入並提交信息的頁面都是表單。用戶輸入信息時,需要進行驗證,確認提供的信息是正確的數據類型。再對這些有效信息進行處理,並將其保存到數據庫的合適地方。這些工作很多都是由Django自動完成的。 在Django中,創建表單的最簡單方式是使用ModelForm,它根據我們在上一章定義的模型中的信息自動創建表單。創建一個名爲forms.py的文件,將其存儲到models.py所在的目錄中,並在其中編寫表單:

from django import forms
from .models import Topic

class TopicForm(forms.ModelForm):  # TopicForm繼承forms.ModelForm
    class Meta:
        model = Topic  # 根據模型Topic創建表單model
        fields = ['text']  # 表單只包含text字段
        labels = {'text': ''}  # 不要爲labels生成標籤

最簡單的ModelForm 版本只包含一個內嵌的Meta 類,它告訴Django根據哪個模型創建表單,以及在表單中包含哪些字段。

這個新網頁的URL應簡短而具有描述性,因此當用戶要添加新主題時,切換到http://localhost:8000/new_topic/。下面是網頁new_topic 的URL模式,將其添加到 learning_logs/urls.py中:


from django.conf.urls import url
from  . import views

urlpatterns = [
    # skip

    # 用於添加新主題的網頁
    url(r'^new_topic/$', views.new_topic, name='new_topic'),

]

編寫視圖new_topic:剛進入new_topic,它顯示一個空表單;對提交的表單數據進行處理,並將用戶重定位到網頁topics:

views.py

from django.shortcuts import render
from .models import Topic
from django.http import HttpResponseRedirect
from  django.urls  import reverse

from .forms import TopicForm
# Create your views here.

#skip

def topic(request, topic_id):
    # skip

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)

導入了HttpResponseRedirect 類,用戶提交主題後將使用這個類將用戶重定向到網頁topics 。函數reverse() 根據指定的URL模型確定URL,這意味着Django 將在頁面被請求時生成URL。還導入了剛纔創建的表單TopicForm。

創建Web應用程序時,將用到的兩種主要請求類型是GET請求和POST請求。對於只是從服務器讀取數據的頁面,使用GET請求;在用戶需要通過表單提交信息時,通常使用POST 請求。處理所有表單時,都將指定使用POST方法。

函數new_topic() 將請求對象作爲參數。用戶初次請求該網頁時,其瀏覽器將發送GET請求;用戶填寫並提交表單時,其瀏覽器將發送POST請求。根據請求的類型,可以確定用戶請求的是空表單(GET請求)還是要求對填寫好的表單進行處理(POST請求)。創建一個TopicForm 實例,將其存儲在變量form 中,再通過上下文字典將這個表單發送給模板。由於實例化TopicForm 時沒有指定任何實參,Django將創建一個可供用戶填寫的空表單。

函數is_valid() 覈實用戶填寫了所有必不可少的字段(表單字段默認都是必不可少的),且輸入的數據與要求的字段類型一致(例如,字段text 少於200個字符,這是我們在上一章中的models.py中指定的)。如果所有字段都有效,調用save() ,將表單中的數據寫入數據庫。保存數據後,就可離開這個頁面了。

創建新模板new_topic.html,用於顯示剛創建的表單:

{% extends "learning_logs/base.html" %}\

{% block content %}
    <p>Add a new topic:</p>

    <form action="{% url 'learning_logs:new_topic' %}" method='post'>
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit" >add topic</button>

    </form>
{% endblock content %}

Django使用模板標籤{% csrf_token %} 來防止攻擊者利用表單來獲得對服務器未經授權的訪問(這種攻擊被稱爲跨站請求僞造 )。只需包含模板變量{{ form.as_p }} ,就可讓Django自動創建顯示錶單所需的全部字段。修飾符as_p 讓Django以段落格式渲 染所有表單元素,這是一種整潔地顯示錶單的簡單方式。 Django不會爲表單創建提交按鈕,因此定義了一個提交的按鈕。

在頁面topics 中添加一個到頁面new_topic 的鏈接:


{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
    <ul> 
       # skip

    </ul>
    <a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}

運行程序顯示了生成的表單,並使用這個表單來添加幾個新主題:

【2】添加新條目

再次定義URL,編寫視圖函數和模板,並鏈接到添加新條目的網頁。在forms.py中再添加一個 類。

創建一個與模型Entry 相關聯的表單,但這個表單的定製程度比TopicForm 要高些:

from django import forms
from .models import Topic, Entry


class TopicForm(forms.ModelForm):  # TopicForm繼承forms.ModelForm
   # skip

class EntryFrom(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text': ''}
        widgets = {'text': forms.Textarea(attrs={'cols': 80})}
        

定義了屬性widgets 。小部件 (widget)是一個HTML表單元素,如單行文本框、多行文本區域或下拉列表。通過設置屬性widgets ,可覆蓋Django選擇的默認小 部件。通過讓Django使用forms.Textarea ,定製了字段'text' 的輸入小部件,將文本區域的寬度設置爲80列,而不是默認的40列。這給用戶提供了足夠的空間,可以 編寫有意義的條目。

在用於添加新條目的頁面的URL模式中,需要包含實參topic_id ,因爲條目必須與特定的主題相關聯。該URL模式如下,將它添加到了learning_logs/urls.py中:


urlpatterns = [
    # skip

    # 添加新條目
    url(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry')

]

這個URL模式與形式爲http://localhost:8000/new_entry/id / 的URL匹配,其中 id 是一個與主題ID匹配的數字。

代碼(?P<topic_id>\d+)/捕獲一個數字值,並將其存儲在變量topic_id 中。請求的URL與這個模式匹配時,Django將請求和主題ID發送給函數new_entry() 。

添加視圖函數new_entry() :

# skip

from .forms import TopicForm,EntryFrom
# skip

def new_entry(request, topic_id):  # 形參topic_id ,用於存儲從URL中獲得的值
    # 在特定的主題中添加新條目
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # 未提交數據:創建一個新表單
        form = EntryFrom()
    else:
        # POST提交數據,對數據進行處理
        form = EntryFrom(data=request.POST)
        print(form)
        if form.is_valid():
            new_entry = form.save(commit=False)   # 實參commit=False,讓Django創建一個新的條目對象,並將其存儲到new_entry 中,但不將它保存到數據庫中。
            new_entry.topic = topic   # 將new_entry 的屬性topic 設置爲在這個函數開頭從數據庫中獲取的主題
            new_entry.save()

            return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id]))   # 列表args ,其中包含要包含在URL中的所有實參

    context = {'topic':topic,'form': form}
    return render(request, 'learning_logs/new_entry.html', context)

添加模板new_entry.html:


{% extends "learning_logs/base.html" %}\

{% block content %}
   <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>  <!-- 讓用戶知道他是在哪個主題中添加條目;該主題名也是一個鏈接,可用於返回到該主題的主頁面。-->
    <p>Add a new entry</p>
    <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'> <!-- topic_id 值,讓視圖函數能夠將新條目關聯到正確的主題-->
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit" >add entry</button>

    </form>
{% endblock content %}

在顯示特定主題的頁面topic.html中添加到頁面new_entry 的鏈接。

【3】編輯條目

來創建一個頁面,讓用戶能夠編輯既有的條目。

.URL模式edit_entry :該頁面的URL需要傳遞要編輯的條目的ID。添加learning_logs/urls.py如下:

   # 用於編輯條目的頁面
    url(r'^edit_entry/(?P<entry_id>\d+)/$',views.edit_entry, name='edit_entry')

在views.py添加視圖函數edit_entry():


def edit_entry(request, entry_id):
    # 編輯既有條目
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if request.method != 'POST':
        # 初次請求,使用當前條目填充表單
        form = EntryFrom(instance=entry)
    else:
        # POST提交的數據,對數據進行處理
        form = EntryFrom(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)

實參instance=entry 創建一個EntryForm 實例,這個實參讓Django創建一個表單,並使用既有條目對象中的信息填充它。用戶將看到既有的數據,並能夠編輯它們。 處理POST請求時,傳遞實參instance=entry 和data=request.POST ,讓Django根據既有條目對象創建一個表單實例。

打開網頁:

模板edit_entry:


{% extends "learning_logs/base.html" %}\

{% block content %}
   <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p> 
    <p>Edit entry</p>
    <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'> 
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit" >save changes</button>

    </form>
{% endblock content %}

鏈接到頁面edit_entry:

在topic.html中添加到頁面edit_entry的鏈接:


{% extends 'learning_logs/base.html' %}
{% block content %}
    # skip
        {% for entry in entries %}
            <li>
                <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
                <p>{{ entry.text|linebreaks }}</p>
                <p><a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a></p>

            </li>
       # skip
{% endblock content %}

2、創建用戶賬戶

目標:將建立一個用戶註冊和身份驗證系統,讓用戶能夠註冊賬戶,進而登錄和註銷。我們將創建一個新的應用程序,其中包含與處理用戶賬戶相關的所有功能。對模型Topic 稍做修改,讓每個主題都歸屬於特定用戶。

【1】應用程序users

先使用命令startapp 來創建一個名爲users 的應用程序:

將應用程序users添加到settings.py中


INSTALLED_APPS = [
    #skip

    # 我的應用程序
    'learning_logs',
    'users'
]

包含應用程序users的URL:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include(('users.urls', "users" ),namespace='users')),
    path('', include(('learning_logs.urls', "learning_logs"),namespace="learning_logs")),
]

登錄頁面:

使用Django提供的默認登錄視圖,URL模式會稍有不同。在目錄learning_log/users/中,新建一個名爲urls.py的文件,並在其中添加代碼:

# 爲應用程序users 定義URL模式

from django.urls import path
from  django.contrib.auth.views import LoginView

from . import views

urlpatterns = [
    # 登錄頁面
    path('login/',LoginView.as_view(template_name='users/login.html'),name="login"),
]

創建頁面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中添加到登錄頁面的鏈接。用戶已登錄時,不想顯示這個鏈接,因此將它嵌套在一個{% 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 。

使用登錄頁面:

登錄一下,看看登錄頁面是否管用。訪問http://localhost:8000/admin/,如果依然是以管理員的身份登錄的,點擊LOG OUT註銷。

註銷後,訪問http://localhost:8000/users/login/:

輸入在前面設置的用戶名和密碼:

註銷:

讓用戶單擊一個鏈接註銷並返回到主頁。爲註銷鏈接定義一個URL模式,編寫一個視圖函數,並在base.html中添加一個註銷鏈接。

註銷URL:

更新users/urls.py:

# 註銷
    path('logout/', views.logout_view, name='logout'),

視圖函數:logout_view()

函數logout_view() :導入Django函數logout() ,並調用它,再重定向到主頁。

users/views.py

from django.shortcuts import render

# Create your views here.

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'))

鏈接到註銷視圖:

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:login' %}">log in</a>
    {% endif %}
</p>

{%  block content %}{%  endblock content %}

僅當用戶登錄後才能看到它:

【4】註冊頁面

使用Django提供的表單UserCreationForm ,但編寫自己的視圖函數和模板。

urls.py:

 # 註冊頁面
    path('register/', views.register, name='register'),

view.py

from django.shortcuts import render
from django.http import HttpResponseRedirect
from  django.urls import reverse
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.forms import UserCreationForm

def logout_view(request):
    # skip

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)

register.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 %}

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 %}

運行網頁:

3、讓用戶擁有自己的數據

創建一個系統,確定各項數據所屬的用戶,再限制對頁面的訪問,讓用戶只能使用自己的數據。 在本節中,修改模型Topic ,讓每個主題都歸屬於特定用戶。這也將影響條目,因爲每個條目都屬於特定的主題。

【1】使用@login_require限制訪問

裝飾器 (decorator)是放在函數定義前面的指 令,Python在函數運行前,根據它來修改函數代碼的行爲。

限制對topics頁面的訪問:

learning_logs/views.py

# skip
from django.contrib.auth.decorators import login_required

# skip

@login_required()
def topics(request):
   # skip

# skip

login_required() 的代碼檢查用戶是否已登錄,僅當用戶已登錄時,Django才運行topics() 的代碼。如果用戶未登錄,就重定向到登錄頁面。 爲實現這種重定向,修改settings.py,讓Django知道到哪裏去查找登錄頁面。請在settings.py末尾添加如下代碼:

# 我的設置
LOGIN_URL = '/users/login/'

全面限制對項目的訪問:

在項目中,不限制對主頁、註冊頁面和註銷頁面的訪問,並限制對其他所有頁面的訪問。

在learning_logs/views.py中,對除index() 外的每個視圖都應用了裝飾器@login_required 。

在未登錄的情況下嘗試訪問這些頁面,將被重定向到登錄頁面。另外,還不能單擊到new_topic 等頁面的鏈接。但如果你輸入URL http://localhost:8000/new_topic/,將重定向到登錄頁面。

【2】將數據關聯到用戶:

要將數據關聯到提交它們的用戶。只需將最高層的數據關聯到用戶,這樣更低層的數據將自動關聯到用戶。例如,在項目“學習筆記”中,應用程序的最高層數據是 主題,而所有條目都與特定主題相關聯。只要每個主題都歸屬於特定用戶,就能確定數據庫中每個條目的所有者。

修改模型Topic:

models.py

from django.db import models
from django.contrib.auth.models import User
# Create your models here.

class Topic(models.Model):
    # 用戶學習的主題
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)  # 建立到模型User 的外鍵關係
   
    def __str__(self):
        # 返回模型的字符串表示
        return self.text

確定當前有哪些用戶:

遷移數據庫時,Django將對數據庫進行修改,使其能夠存儲主題和用戶之間的關聯。爲執行遷移,Django需要知道該將各個既有主題關聯到哪個用戶。最簡單的辦法是,將既有主題都關聯到同一個用戶,如超級用戶。爲此,我需要知道該用戶的ID。

查看已創建的所有用戶的ID。啓動一個Django shell會話,並執行如下命令:

Django詢問要將既有主題關聯到哪個用戶時,指定其中的一個ID值。

遷移數據庫:

爲將所有既有主題都關聯到管理用戶ll_admin,輸入了用戶ID值1。並非必須使用超級用戶,而可使用已創建的任何用戶的ID。接下來,Django使用這個值來遷移數據庫,並生成了遷移文件0003_topic_owner.py,它在模型Topic 中添加字段owner 。 現在可以執行遷移了。爲此,在活動的虛擬環境中執行下面的命令:

Django應用新的遷移,結果ok。 爲驗證遷移符合預期,可在shell會話中像下面這樣做:

【3】只允許用戶訪問自己的主題:

在views.py中,對函數topics() 做如下修改:


@login_required()
def topics(request):
    # 顯示所有主題
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')  # 讓Django只從數據庫中獲 取owner 屬性爲當前用戶的Topic 對象
    context = {'topics':topics}
    return render(request, 'learning_logs/topics.html',context)

用戶登錄後,request 對象將有一個user 屬性,這個屬性存儲了有關該用戶的信息。

【4】保護用戶的主題:

由於還沒有限制對顯示單個主題的頁面的訪問,因此任何已登錄的用戶都可輸入類似於http://localhost:8000/topics/1/的URL,來訪問顯示相應主題的頁面。

在視圖函數topic() 獲取請求的條目前執行檢查:

# skip

from django.http import HttpResponseRedirect, Http404
# skip
@login_required
def topic(request, topic_id):
    # 顯示單個主題及其所有的條目
    topic = Topic.objects.get(id=topic_id)
    # 確認請求的主題屬於當前用戶
    if topic.owner != request.user:   #如果請求的主題不歸當前用戶所有,就引發Http404異常
        raise Http404  
    
    entries = topic.entry_set.order_by('-date_added')
    context = {'topic':topic,'entries':entries}
    return  render(request, 'learning_logs/topic.html', context)

# skip

現在查看其他用戶的主題條目,將看到Django發送的消息Page Not Found。

【5】保護頁面edit_entry

頁面edit_entry 的URL爲http://localhost:8000/edit_entry/entry_id / ,其中 entry_id 是一個數字。下面來保護這個頁面,禁止用戶通過輸入類似於前面 ]的URL來訪問其他用戶的條目:

views.py

@login_required
def edit_entry(request, entry_id):
    # 編輯既有條目
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404
    if request.method != 'POST':
        # 初次請求,使用當前條目填充表單
        form = EntryFrom(instance=entry)
   # skip

【6】將新主題關聯到當前用戶

view.py

@login_required
def new_topic(request):
    # 添加新主題
    if request.method != 'POST':
        # 未提交數據:創建一個新表單
        form = TopicForm()
    else:
        # POST提交數據,對數據進行處理
        form = TopicForm(request.POST)
        if form.is_valid():
            new_topic = form.save(commit=False)  # 先修改新主題,再將其保存到數據庫中
            new_topic.owner = request.user # 將新主題的owner 屬性設置爲當前用戶
            new_topic.save()
            return HttpResponseRedirect(reverse('learning_logs:topics'))

    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

現在,這個項目允許任何用戶註冊,而每個用戶想添加多少新主題都可以。每個用戶都只能訪問自己的數據,無論是查看數據、輸入新數據還是修改舊數據時都如此。

4、小結

學習了:

  • 如何使用表單來讓用戶添加新主題、添加新條目和編輯既有條目;
  • 如何實現用戶賬戶。讓老用戶能夠登錄和註銷;
  • 如何使用 Django提供的表單UserCreationForm 讓用戶能夠創建新賬戶;
  • 建立簡單的用戶身份驗證和註冊系統後,通過使用裝飾器@login_required 禁止未登錄的用戶訪問特定頁面;
  • 通過使用外鍵將數據關聯到特定用戶;
  • 如何執行要求指定默認數據的數據庫遷移;
  • 如何修改視圖函數,讓用戶只能看到屬於他的數據。使用方法filter() 來獲取合適的數據;
  • 如何將請求的數據的所有者同當前登錄的用戶進行比較。

下一章:Web應用程序之樣式設置                                           上一章:Web應用程序之樣式設置

 

發佈了81 篇原創文章 · 獲贊 16 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章