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应用程序之样式设置

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值