建立社交网站

在之前我们建立的博客网站,更精确一点说应该称为个人博客网站,唯一增删改查文章的手段是作为管理员的我们通过后台实现。这种类似的有新闻站点等内容提供站点,一般只开放评论。

如果想要做一个类似于提供公开博客服务的网站,这种网站其实更多的带有社交属性,即需要管理不同的用户的内容。这次就来做一个社交网站,引入一个对于现代web开发非常重要的功能:用户身份验证。

在开始之前,还是必须分析一下需求和先行思考一下。这个网站需要具备的基础功能有:

  • 像其他很多社交网站一样,必须有完整的用户身份验证系统
  • 可以关注其他人
  • 分享内容给其他用户
  • 可以查看关注的人的新内容

这一章就做用户身份系统,django里用户身份验证也是一大功能模块,这一章主要内容有:

  • 使用django内置的用户模块进行基础的登录,登出,修改密码,修改用户信息操作
  • 建立用户注册功能
  • 扩展用户数据,加入自定义的用户信息
  • 加入第三方社交平台验证功能

使用virtual env 启动新项目

如果是多个站点,其实也可以做成一个功能,然后提供不同的入口即可,不过这次还是新开一个项目叫做bookmarks,然后新建一个app叫做account。

不过这次用新东西,就是virtual env来配置环境。在Pycharm里找到自己新建的bookmarks目录,新建Django项目,然后使用virtual env 而不是系统的解释器,不继承所有系统安装的包。然后回车

这样就在bookmarks目录下新建了同名django项目,而且这个项目是一个虚拟环境,目前只有默认的python 解释器和pip,然后在启动过程中,我们看到Pycharm自动给我们装好了最新的django。并且在bookmarks目录下建立了bookmarks项目

这次为了避免像第一章里出现过的taggit0.22与django 2.1有冲突需要升级到0.23版的问题,我们就先卸载自动安装的最新django 2.1.1,转而安装django 2.0.5,然后新建account app。

之后就是配置APP,用户这里目前因为要使用django内置的用户模块,就先migrate一下让django默认的数据表生成。

内置验证模块的使用

由于用户身份验证是广泛使用的功能,所以django内部有验证模块 django.contrib.auth 提供了相关功能。其实第一章里通过manage.py 建立超级用户,就调用了auth模块里的功能。实际上,在使用startproject创建项目的时候,auth模块就会被自动加入到INSTALLED_APPS里,然后在中间件里还会启用 AuthenticationMiddlewareSessionMiddleware 两个中间件,这两个中间件也是用户验证的一部分。其中第一个中间件是用来处理带有session的HTTP请求,生成一个user对象方便使用;第二个中间件就是处理当前HTTP请求中间的session信息。

中间件其实就是django 的本体,在后边还会学到如何使用中间件以及自定义中间件。

内置验证模块除了上边的auth模块和两个中间件之外,还包含其他一些内容:

  • User 数据类,常用字段有username, password, email等,对应默认的auth_user表
  • Group 数据类,表示用户组,对应默认的auth_user_groups表
  • Permission 数据类,表示权限。这一类有几个表,分别是通用权限,用户权限,组权限,对应默认的三个permission结尾的表

这些类晚些时候都会用到。操作这些类其实就是操作migrate后默认生成的那些表格。

建立登录视图

想一想登录的页面,一个登录视图的功能至少得有:

  • 显示一个页面供填写用户名和密码,然后通过表单接受数据
  • 到数据库里比对用户名和密码是否正确
  • 如果成功,需要确定并保持一个登陆状态,通过在HTTP请求上附加session来实现

确定了实现的功能,就开始编写,还是老四步 model –> views –> template –>url

其中模型我们注意到,已经有了默认的User类,所以建立表单:

from django import forms


class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)

比较简单,需要注意的就是密码不能采用普通input文本,必须使用type=”password”的input,所以要修改默认的widget

然后编写视图:

from django.shortcuts import render, HttpResponse
from django.contrib.auth import authenticate, login
from .forms import LoginForm


def user_login(request):
    if request.method == "POST":
        form = LoginForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            user = authenticate(request, username=cd['username'], password=cd['password'])
            if user is not None:
                if user.is_active:
                    login(request, user)
                    return HttpResponse("Authenticated successfully")
                else:
                    return HttpResponse("Disabled account")
            else:
                return HttpResponse("Invalid login")

    else:
        form = LoginForm()

    return render(request, 'account/login.html', {'form': form})

这个视图逻辑使用很多内置方法。首先导入了系统自带的认证和登录两个方法。表单相关的内容不再解释了。核心是authenticate方法的使用。

authenticate其实默认就是去操作User类,传入两个字段分别等于表单验证成功的用户名和密码数据,之后到数据库中比对,如果成功,则返回对应的user对象,如果不成功,就是一个空值。

所以之后就可以判断用户是否为None,如果不为None而且处于活动状态表示登录成功,就调用login方法,这个方法接受request和user两个参数,会给当前的请求附加上session表示登录成功。如果用户不活动,则显示用户被禁止。

如果用户直接就返回了None,说明用户名或者密码错误。在表单填写错误的时候返回原来的表单,GET请求的时候返回空白表单。

之后在account应用下建立urls.py,这个没什么好说的:

from django.urls import path
from . import views

urlpatterns = [
    # post views
    path('login/',views.user_login,name='login')
]

再去主路由增加一行:

path('account/', include('account.urls'))

然后老样子,在account应用下建立template目录,之后建立base.html和account目录,在account目录下边建立login.html

base.html的内容如下:

{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<span class="logo">Bookmarks</span>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>

这里引入了静态文件,于是把static文件夹从源码COPY到account应用目录下。这个基础模板也很简单,除了title之外,只有一个叫做content的块供替换。然后继续写login.html

{% extends 'base.html' %}

{% block title %}Log-in{% endblock %}

{% block content %}
<h1>Log-in</h1>
<p>Please, use the following form to log-in:</p>
    <form action="." method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Log in"></p>
    </form>
{% endblock %}

这部分也没什么好说的,就展示一个Form。

想启动项目了,但是突然想起来还没有增加过任何用户,那就先manage.py一个管理员。管理员的信息其实已经知道了,就是写到了auth_user表里。

只有一个管理员用户有点少,启动项目,然后进入管理后台,新建一个用户。

之后到account/login,可以发现有了登录界面,各种测试一下这个页面。发现可以登录也有表单验证功能。

管理后台和登录页面的示例如下:

使用django内置登录和登出功能

前边的这个登录函数从效果上来看仅仅是展示了登录成功与否的信息。实际上其中的login内置方法做了一件很重要的事情,就是给当前的请求附加了session。实际上已经验证了当前用户的身份。

这个user_login视图还是我们自行编写的,导入很多内置的方法,实际上,大部分情况下可以直接使用django的内置用户验证功能,这些功能都在django.contrib.auth.views里,都是CBV,有这么几个:

  • LoginView 处理一个login 表单和登录功能(和我们写的功能类似)
  • LogoutView 退出登录
  • PaswordChangeView 处理一个修改密码的表单,然后修改密码
  • PasswordChangeDoneView 修改密码成功后的重定向方法
  • PasswordResetView 启动修改密码的流程,生成一个一次性链接和对应的验证token然后发送到用户的邮件地址
  • PasswordResetDoneView 告诉用户已经发送给了他们一封邮件重置密码
  • PasswordResetConfirmView 处理用户重置密码的页面
  • PasswordResetCompleteView 成功修改密码后的重定向功能

使用上边的内置方法,可以在建立一个站点的时候节约很多时间,因为编写重置密码的功能还是有些复杂的。这些CBV的一些默认参数都可以修改,经常修改的就是让这些CBV去渲染哪些模板以及使用的表单数据来源。

所有内置验证方法的官方文档看这里

现在就来用内置验证方法,就不需要编写views.py里的内容了,可以直接将urls配置到内置的功能上去,将我们自己的登录函数的url注释掉,然后直接匹配内置功能:

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
urlpatterns = [
    # path('login/', views.user_login, name='login'),
    path('login',auth_views.LoginView.as_view(),name='login'),
    path('logout',auth_views.LogoutView.as_view(),name='logout'),
]

这里就直接把登录和登出导向了内置CBV,然后要解决的问题就是这些CBV渲染什么模板。在templates目录下新建一个registration的目录,这个目录就是使用内置方法的时候,默认会去到当前应用的模板目录里寻找具体模板的地址。

这里还一个要点是,INSTALLED_APPS里注册应用的顺序对于使用什么模板有关系,django.contrib.admin里带了一些验证模板。我们把account应用放到admin应用的上边去,让django默认使用我们自己的模板。

来建立我们自己的模板,在registration目录下建立一个login.html:

{% extends 'base.html' %}

{% block title %}Log-in{% endblock %}

{% block content %}
    <h1>Log-in</h1>
    {% if form.errors %}
        <p>
        Your username and password didn't match.
        Please try again.
        </p>
    {% else %}
        <p>Please, use the following form to log-in:</p>
    {% endif %}

    <div class="login-form">
        <form action="{% url 'login' %}" method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <input type="hidden" name="next" value="{{ next }}">
            <p><input type="submit" value="Log-in"></p>
        </form>
    </div>

{% endblock %}

这个模板和刚才自行编写的比较类似,但是由于视图不是我们自己编写的,有几点需要解释:

  • 内置视图Login默认使用django.contrib.auth.forms里的AuthenticationForm,并且向模板传一个form变量
  • 使用form.errors就可以拿到是否验证错误的结果
  • 有一个隐藏的input标签,用于存放从哪个页面跳转过来URL,如果有的话,在成功验证之后,会跳转回原来的页面。

相同位置建立 logged_out.html

{% extends 'base.html' %}

{% block title %}
Logged out
{% endblock %}

{% block content %}
<h1>Logged out</h1>
    <p>You have been successfully logged out. You can <a href="{% url 'login' %}">log-in again</a>.</p>
{% endblock %}

这个也很直接,没有什么太多要说的,就是一个退出登录后跳转的页面。

现在用户登录和退出登录的功能都好了,需要做一个用户登录以后默认显示的页面。由于这个页面只能够在登录以后才显示,所以需要对这个页面的控制视图做一些处理。在views.py里添加:

from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
    return render(request, 'account/dashboard.html', {'section': 'dashboard'})

导入了一个装饰器,装饰了显示欢迎页的视图函数,这样装饰以后,这个视图函数就只能在用户已经登录的状态下才会执行。如果没有登录,会把用户重定向到login对应的URL然后还会加上一个GET请求的参数叫next,正好可以被loginLoginView拿到。

多出来的这个参数section,可以用来追踪用户当前所在的页面,这样可以很方便的知道当前页面是哪个视图函数在处理。

然后在templates/account/目录下建立欢迎页面dashboard.html:

{% extends 'base.html' %}

{% block title %}
Dashboard
{% endblock %}

{% block content %}
    <h1>Dashboard</h1>
    <p>Welcome to your dashboard.</p>
{% endblock %}

最后配置account应用的urls.py:

path('',views.dashboard,name='dashboard'),

这行表示到account里就默认启动欢迎页面,如果没登录,就会跳转到登录界面。

还有需要在settings.py里做些设置:

LOGIN_REDIRECT_URL = 'dashboard'
LOGIN_URL = 'login'
LOGOUT_URL = 'logout'

这三行分别表示:

  • 如果没有next参数,登录成功后重定向到哪个URL名称
  • 将用户重定向到登录页面的时候,定向到哪个URL名称
  • 将用户重定向到登出页面的时候,定向到哪个URL名称

一下子做了很多事情,回头整理一下:

  • 将URL直接定向到内置的Login 和Logout CBV上
  • 为CBV和欢迎页面编写了模板,其中的变量要用CBV返回的变量名
  • 为欢迎页面使用了装饰器保证只有登录状态才能访问,如果未登录就去找Login功能
  • setting.py里规定了默认重定向到登录和登出功能的URL

最后一步需要在页面上加上登录和登出链接,考虑到这里,就会发现除了登录和登出,还必须展示相应的内容,不能用户登录的情况下还显示登录,也不能在未登录的情况下显示登出。这个时候就有个新问题:如何判断用户是否登录或者登出呢?

之前已经回答过这个问题,那就是LoginView会在HTTP请求里附加session来标明当前访问的登录状态,直接使用session比较麻烦。其实一开始说的中间件,会在此时的request上增加一个对象叫做User,即使没有登录,也可以拿到这个User对象,只不过这个User对象是一个AnonymouseUser的实例,可以使用User.is_authenticated来判断该用户是否是登录后的用户。

知道了如何判断用户状态,就可以修改base.html了,这次只修改 id=header 的那个div标签,让其中的内容根据用户来变动。:

<div id="header">
<span class="logo">Bookmarks</span>
    {% if request.user.is_authenticated %}
    <ul class="menu">
        <li {% if section == 'dashboard' %}class="selected"{% endif %}><a href="{% url 'dashboard' %}">My dashboard</a></li>
        <li {% if section == 'images' %}class="selected"{% endif %}><a href="#">Images</a></li>
        <li {% if section == 'people' %}class="selected"{% endif %}><a href="#">People</a></li>
    </ul>
    {% endif %}

    <span class="user">
        {% if request.user.is_authenticated %}
        Hello {{ request.user.first_name }},{{ request.user.username }},<a href="{% url 'logout' %}">Logout</a>
            {% else %}
            <a href="{% url 'login' %}">Log-in</a>
        {% endif %}
    </span>
</div>

这一段的逻辑主要是判断用户登录与否,如果未登录,UL部分不显示,只显示一个登录链接。如果用户已经登录,则显示全部内容的链接以及登出链接。

现在启动项目,到登录页面看一下。发现实现了登录和登出的功能:

先别急着过这一部分,这一本部分的逻辑其实非常重要,值得反复揣摩,用base.html展示基本的用户登录信息,然后用具体页面对应的视图来做登录和登出的操作。

这一部分的逻辑和博客项目有点类似,横跨多个页面的逻辑,放在base.html里,单独页面的逻辑,用视图函数来操作。

使用Django内置的修改密码功能

用户的登录和登出状态解决了,然后很重要的就是用户的密码相关操作。和上边一样,直接使用内置功能的话,把urls定向到内置功能,然后编写模板即可。

path('password_change', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),

第一行的视图会控制渲染修改密码的页面和表单,第二个视图会进行成功修改密码之后的操作,为两个视图建立模板:

先在registration目录下建立 password_change_form.html:

{% extends 'base.html' %}

{% block title %}
Change your password
{% endblock %}

{% block content %}
<h1>Change your password</h1>
    <p>Use the form below to change your password.</p>
    <form action="." method="post" novalidate>
    {{ form.as_p }}
    <p><input type="submit" value="Change"></p>
    {% csrf_token %}
    </form>
{% endblock %}

再建立password_change_done.html:

{% extends 'base.html' %}

{% block title %}
Password changed
{% endblock %}

{% block content %}
<h1>Password changed</h1>
    <p>Your password has been successfully changed.</p>
{% endblock %}

启动服务,到 http://127.0.0.1:8000/account/password_change/ 看一下,如果未登录则会被要求登录。之后可以成功的修改密码。

修改密码的示例页面如下:

修改密码成功的示例页面如下:

重置密码的功能

同样步骤操作,把urls配置到内置方法上:

path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

这里要解释的就是第三行,实际上是因为这个视图需要两个参数传进去,所以就写成了这样,这是固定的写法,因为需要通过一个固定的链接来访问这个方法。

然后继续编写模板 password_reset_form.html

{% extends 'base.html' %}

{% block title %}
Reset your password
{% endblock %}

{% block content %}
<h1>Forgotten your password?</h1>
    <p>Enter your e-mail address to obtain a new password.</p>
    <form action="." method="post" novalidate>
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Send e-mail"></p>
    </form>
{% endblock %}

这个页面已经没有什么需要解释的了,示例如下:

再建立发送邮件的页面 password_reset_email.html:

Someone asked for password reset for email {{ email }}. Follow the link
below:
{{ protocol }}://{{ domain }}{% url "password_reset_confirm" uidb64=uid token=token %}
Your username, in case you've forgotten: {{ user.get_username }}

这里的几个变量都是默认的视图里传进来的,要记住写法。尤其是生成URL的写法。

之后再来一个password_reset_done.html,表示成功发送邮件的页面:

{% extends 'base.html' %}

{% block title %}
Reset your password
{% endblock %}

{% block content %}
<h1>Reset your password</h1>
<p>We've emailed you instructions for setting your password.</p>
<p>If you don't receive an email, please make sure you've entered the
address you registered with.</p>
{% endblock %}

成功发送邮件的示例页面如下:

然后建立重置密码的页面password_reset_confirm.html,这个页面是用户从邮件中打开链接到达的页面:

{% extends 'base.html' %}

{% block title %}Reset your password{% endblock %}

{% block content %}
    <h1>Reset your password</h1>
    {% if validlink %}
        <p>Please enter your new password twice:</p>
        <form action="." method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <p><input type="submit" value="Change my password"/></p>
        </form>
    {% else %}
        <p>The password reset link was invalid, possibly because it has
            already been used. Please request a new password reset.</p>
    {% endif %}
{% endblock %}

这个页面里有一个判断就是链接是否有效,也是固定用法。如果有效就显示修改密码的表单,无效就只显示一段信息。

这个页面的示例如下:

最后就是成功修改密码的页面password_reset_complete.html:

{% extends "base.html" %}
{% block title %}Password reset{% endblock %}
{% block content %}
<h1>Password set</h1>
<p>Your password has been set. You can <a href="{% url "login" %}">log in
now</a></p>
{% endblock %}

该页面的示例如下:

这些已经都没什么好解释的了,只要记住这一批页面及对应的函数就可以了。最后在登录页面加上忘记密码功能的链接:

<p><a href="{% url 'password_reset' %}">Forgotten your password?</a></p>

然后启动站点。功能可以测试一下,都OK了,记得也要配置邮件发送服务器,否则在发送邮件的时候会出错。

最后还有一个要点,要不怎么说django建站简单粗暴,那就是在account里边我们配置的所有这些和用户相关的URL,除了dashboard那句,剩下的log,都可以用一句话来替代:

path("", include('django.contrib.auth.urls')),

再测试一下,完全和之前相同,也就是说在当前的空间里引入了另外一个二级解析,把之前手工设置的log系列,password系列和reset系列都对应到了内置CBV上去了,这里可以看到django.contrib.auth.urls.py的源码,其实就是和我们编写的一样。

这一部分也是相当核心的内容,只要记住这个使用内置验证模块的套路,开发带有用户功能的网站,就不用花费时间建立基础的用户认证功能,主要精力可以放在基于用户角色和功能的设计上。记住如果要使用自定义的模板,就在settings.py里把自己的APP放到admin APP的上边,然后在当前应用模板的registration目录下建立那一套模板。

用户注册与完善用户信息

在之前通过内置auth模块,简单快捷的完成了用户功能,也掌握了基本的用户界面的开发步骤和通过base.html显示用户登录情况技巧。在完成用户的验证功能之后,现在的新增用户还只能够通过后台来完成,但是实际的社交网站,肯定需要用户注册,而且用户的各种信息,也肯定比django内置的只有姓名和邮件要丰富很多。这一部分就先来做用户注册功能。

开始之前依然是想一下思路,所谓用户注册,其实就是用户来完成向数据库中添加用户相关数据的操作,因此我们也需要一个视图操作一个模板和表单,让用户按照表单提交资料,验证通过后就新增一个用户。

用户注册

继续老流程,需要注册用户,先分析需求建立model,这里为了方便,因为在之前的验证里我们使用了内置模块,而内置模块使用默认的auth.user表,因此我们就基于User类来直接建立表单,编辑forms.py:

from django.contrib.auth.models import User
class userRegistrationForm(forms.ModelForm):
    password = forms.CharField(label='password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Repeat password', widget=forms.PasswordInput)

    class Meta:
        model = User
        fields = ('username','first_name','email')

    def clean_password2(self):
        cd = self.cleaned_data
        if cd['password'] != cd['password2']:
            raise forms.ValidationError(r"Password don't match.")
        return cd['password2']

导入了User类,然后继承ModelForm建立表单,Meta类中指定model为User,只包含username,first_name和email三个字段。这里边隐含了一些验证,比如username字段是unique=True即不能重复的。我们没有直接使用password字段,因为用户输入的密码不可能直接放入到数据库,一般都是要重复比对一下才能够放入数据库中。而且密码在数据库中也不是明文存储的,需要加密,因此还需要一些其他处理。

还定义了一个方法.clean_password2,这个方法的名字不是随便起的,而是按照clean_字段名来起的,函数的返回值也是规定好的,如果不满足条件必须报ValidationError错误。这样的函数,在进行表单的is_valid()调用时,会自动被执行,如果报错,则表单无法通过验证。也就是说,我们为password2字段加了一个自定义的验证器。

这里为什么我们还没有验证完,就已经有了cleaned_data呢,这里有一篇介绍django验证表单顺序的文章,可以看到,在执行自定义验证器之前,已经执行了每个字段的.clean()方法,这个方法仅针对字段属性进行验证,只要这个通过了,cleaned_data中就有了数据,之后才是执行自定义验证器也就是clean_字段名,最后执行form.clean()完成验证。如果最后一步出错,cleaned_data里只剩有效的值,.errors属性内就有了错误信息,不再为空。当然过程中任意时候抛出ValidationError都是这样。

如果不想自行建立表单,django.contrib.auth.forms里有一个UserCreationForm可以直接使用,和我们建立的表单很类似。

然后编写视图:

from .forms import LoginForm, UserRegistrationForm

def register(request):
    if request.method == "POST":
        user_form = UserRegistrationForm(request.POST)
        if user_form.is_valid():
            new_user = user_form.save(commit=False)
            new_user.set_password(user_form.cleaned_data['password'])
            new_user.save()
            return render(request, 'account/register_done.html', {'new_user': new_user})
    else:
        user_form = UserRegistrationForm()
    return render(request, 'account/register.html', {'user_form': user_form})

视图逻辑也很简单,唯一的要点就是密码,不能够用明文,在验证通过之后,使用内置的.set_password(raw_password)来将明文密码转换为密文,然后再存储。

再配置account应用的urls:

path('register/', views.register, name='register'),

最后是建立模板register.html和register_done.html,也轻车熟路了:

{#register.html#}
{% extends 'base.html' %}

{% block title %}
Create an account
{% endblock %}

{% block content %}
<h1>Create an account</h1>
    <p>Please, sign up using the following form:</p>
    <form action="." method="post" novalidate>
    {{ user_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Register"></p>
    </form>
{% endblock %}

{#register_done.html#}
{% extends 'base.html' %}

{% block title %}
Welcome
{% endblock %}

{% block content %}
    <h1>Welcome {{ new_user.first_name }}!</h1>
    <p>Your account has been successfully created. Now you can <a href="{% url 'login' %}">log in</a>.</p>
{% endblock %}

现在可以打开account/register/来看看变化了。可以成功的注册了,还要像现在很多网站一样在登录页面内加上注册链接就行了。

注册页面和注册成功页面的示例如下:


完善用户信息

到这里可以发现,实际上我们是完成了对于User表单的增改查(删除用户的功能一般很少对用户开放,因为用户信息对于网站来说也是一个资源)。增就是注册,改主要是改密码,用户名一般不会让修改。查就是在登录的时候查表进行验证。实现了对于User的完整操作。

这个时候再回到社交网站的需求上来,默认的User表只有姓名,邮件等信息,而社交网站需要用户提供的信息远不止这些,一般可能有更详细的身份信息,爱好等用于社交功能。

所以很显然,还需要建立其他表来存放更多的用户信息,这个表肯定和User表是一对一的关系,实际上就等于在User表上新加了字段,但是不能修改表,所以就通过一对一的方式建立新的数据表存放用户信息。

在models.py里编写:

from django.db import models
from django.conf import settings


class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    date_of_birth = models.DateField(blank=True, null=True)
    photo = models.ImageField(upload_to='user/%Y/%m/%d/', blank=True)

    def __str__(self):
        return "Profile for user {}".format(self.user.username)

这里需要解释的是:OneToOneField一对一的第一个参数是一个数据类,可以导入django.contrib.auth.model.User来当做参数,但这里是为了表示这是引入的内置类(装逼),而采用了导入setting的方法,知道还有用大写属性表示内置类的装逼方法即可。

还有一个要点是,引入了图片ImageField类,这个类在ORM里必须使用Python的Pillow库,因此还需要安装Pillow库。而且现在由于要开放用户上传头像这种媒体文件(静态文件),还必须在settings.py里配置一下放媒体的目录:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

第一行表示处理用户上传文件的URL名称,第二行表示媒体文件的存放目录,BASE_DIR是动态生成的项目的根目录,这里也是为了动态生成媒体文件的存放目录。之后在项目根目录下建立media文件夹。

来编辑一下项目的根urls.py:

from django.conf import settings
from django.conf.urls.static import static

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

django.conf其实就是settings.py里的配置。static()方法在开发的时候可以使用,但是正式生产环境是不能够使用static路径的,因为WSGI服务不会从Django的文件夹里找静态文件。这个配置就是告诉Django DEBUG模式下就到media目录里去找静态文件。

新数据库有了,先migrate一下生成account_profile数据表,然后在应用的admin.py里注册到管理后台中:

from django.contrib import admin
from .models import Profile

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'date_of_birth', 'photo']

之后就是让用户填写自己的用户信息了,所以根据Profile类生成表单,编写forms.py:

from .models import Profile

class UserEditForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileEditForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('date_of_birth', 'photo')

一个表单来自User类,一个表单来自Profile类,一会我们把表单放到一个HTML FORM标签里就完事了。现在来写视图:

# 先导入Profile类,然后在register视图的new_user.save()下边增加
from .models import Profile
Profile.objects.create(user=new_user)

这是因为一对一就相当于unique的外键,必须在用户创建的时候在Profile表里直接做好关联,否则后边直接添加用户信息的时候就不知道要取哪个信息。

剩余的视图业务就很清晰了,当用户在登录状态添加用户信息时候,给当前用户提供一个表单供其填写,然后将数据根据是哪一个用户,分别填入到Profile和User表中即可。

from .forms import UserEditForm, ProfileEditForm

@login_required
def edit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

在原来的类似的视图中,直接就用表单类加上request.POST生成了表单类的实例。这里的instance参数可以接受一个model的子类作为参数,在例子里就是在实例化user_form和profile_form的时候,并不是实例化一个新的类,而是实例化user和user.profile对应的那一行数据,之后的操作都是在保存这行数据。否则就变成了去增加新的一行了。

可见instance参数在修改已经存在的数据表的时候需要使用,之前的视图都是在数据库里新增数据,所以就直接实例化了新类。

这个视图其他的逻辑都很简单,成功就修改数据,不成功就按照当前用户生成两个空白的表单继续展示。注意两个表单都要带有instance=当前用户和用户profile才行。

之后编写urls和模板:

# urls.py
path('edit/', views.edit, name='edit'),

{#edit.html#}
{% extends 'base.html' %}

{% block title %}
Edit your account
{% endblock %}

{% block content %}
<h1>Edit your account</h1>
    <p>You can edit your account using the following form:</p>
    <form action="." method="post" enctype="multipart/form-data" novalidate>
    {{ user_form.as_p }}
    {{ profile_form.as_p }}
    {% csrf_token %}
        <p><input type="submit" value="Save changes"></p>
    </form>
{% endblock %}

唯一要说的就是form里enctype是上传文件必须写的,其他就是把两段表单放到一起。现在启动站点,到account/edit/路径下边看看效果。

这里要注意的是,很可能已经添加的用户无法修改,是因为 Profile 的外键中没有保存值。所以会出错,需要新建用户才可以正常修改内容。

之后在dashboard.html里给用户添加修改用户信息的链接即可:

<p>You can <a href="{% url 'edit' %}">edit your profile</a> or <a href="{% url "password_change" %}">change your password</a>.</p>

这样就通过额外增加了一个表,保存了额外的用户信息,自定义多少用户信息都没有问题了。

用户编辑资料的示例页面如下:

使用消息功能

很多时候,网站在用户完成一些操作的时候,需要向用户发送一次性消息,django有一个内置模块django.contrib.messages已经包含在初始化的INSTALLED_APPS里,还有一个默认启用的中间件叫做 django.contrib.messages.middleware.MessageMiddleware,共同构成了消息系统。

消息系统的工作原理是:消息默认存储在cookie里,在用户执行了下次请求的时候,就会显示消息。

message模块的用法:

  • 导入 django.contrib import messages
  • messages.errors(request, ‘error message’) 用来生成一条消息附加在request上
  • .success() 一个动作成功之后发送的消息
  • .info() 通知性质的消息
  • .warning() 警告性质的内容,所谓警告就是还没有fail但很可能fail的东西
  • .error() 动作失败的通知
  • .debug() 除错信息,在生产环境中需要被移除

准备在我们的站点中增加消息内容,由于消息也是贯穿整个网站的,只要发送就想让用户在下一次刷新页面的时候看到,那么还是打算将消息的内容包含在base.html中。

这里额外说一下的就是settings.py里的TEMPLATES设置中,有一个context_processors。之前说过不用视图函数传入,模板中可以随时使用user变量,就是因为这里包含了.auth模块,才提供了上下文变量user。这里默认也有messages模块,所以不需要视图函数传入,在模板里可以直接使用messages变量。知道了这个以后,就可以来编辑base.html了:

{#在 #header 和 #content 的两个div之间插入#}
{% if messages %}
    <ul class="messages">
    {% for message in messages %}
    <li class="{{ message.tags }}">{{ message|safe }}<a href="#" class="close">X</a></li>{% endfor %}
    </ul>
{% endif %}

在base.html中增加了显示消息的模块之后,找个视图函数修改一下,在完成某个功能之后增加messages即可,比如edit视图,在成功之后添加一条成功信息:

from django.contrib import messages

@login_required
def edit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

增加了两条语句,在成功增加之后显示成功信息,在表单验证失败的时候显示错误信息。之后可以到站点内实验一下。

显示各种消息的示例如下:

自定义用户验证后端

学习编程,总归都是要从先使用内置功能,逐步走上自定义开发路径。

用户验证的设置在settings.py里有一个 AUTHENTICATION_BACKENDS 的设置列出了哪些验证后端可以被使用。

默认的settings.py里没有配置这个设置,使用的默认是 django.contrib.auth.backends.ModelBackend,这个ModelBackend就是指的使用django.contrib.auth模块。

在使用内置模块的.authenticate()方法的时候,就会到settings.py里按照AUTHENTICATION_BACKENDS的设置一个一个的后端验证过来,直到某一个返回成功为止。如果都失败,用户就无法登陆。

所以如果要编写一个自定义的验证模块,这个模块是一个类,必须提供如下的方法:

  • .authenticate():这个方法使用request对象和用户验证信息,返回一个符合验证条件的user对象。否则就返回None。request对象是HttpRequest对象。
  • .get_user():传入一个用户ID,返回一个用户对象

下边就来编写一个自定义的验证类,用于通过电子邮件登录。在account应用目录下建立 authentication.py:

from django.contrib.auth.models import User


class EmailAuthBakcend(object):
    """
    Authenticate using an e-mail address.
    """

    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            return None

这个就是直接把上边的文字部分写了出来。逻辑也很简单。然后在settings.py里加入下边的内容:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
]

将自己定义的验证类加入到了默认验证的后边,注意,这里的次序也是验证的次序,和配置INSTALLLED_APPS类似。通过上边的分析,现在可以用用户名或者电子邮件来登录了。

增加第三方验证 Social authentication

像国内的很多网站上,可以看到通过QQ,微信,新浪登录的链接,点击之后会从另外一个社交服务提供商获取资源并让你登录,这个就是social authentication。

Python Social Auth就是一个第三方模块可以用来进行第三方验证。这个模块为很多Python下的web框架提供了验证模块,django也在支持的范围内。先安装:

pip install social-auth-app-django==2.1.0

从名字可以看出来,没有安装完整的social-auth,只安装了它作为django app的模块。

在INSTALLED_APPS里添加:

INSTALLED_APPS = [
    #...
    'social_django',
]

然后由于新APP肯定自带了模型,migrate一下,可以看到新增social_auth开头的一系列表。social-auth的文档列出了所有可以支持的第三方网站,例子中来使用最流行的Facebook, Twitter和Google。

由于第三方应用不需要编写模型也不需要编写视图,只需要了解API,所以下一步就是配置URL,social-auth里已经配置好了url,只要添加到项目主路由上就行了。

    path('social-auth/', include('social_django.urls',namespace='social')),

注意,有些第三方网站是不允许将用户重定向到类似127.0.0.1的本地回环地址的,所以必须给自己的主机起个名字,修改系统的hosts文件,加上:

127.0.0.1 mysite.com
# 还需要在settings.py的 ALLOWED_HOSTS里加上hosts文件里自定义的主机名称
ALLOWED_HOSTS = ['mysite.com', 'localhost', '127.0.0.1']

Facebook 第三方登录

之前对social-auth的配置进行完了,现在开始分别对Facebook,Twitter 和 Google增加验证。

还记得上边说过的验证后端吗,每一个验证都需要新增加一个验证后端:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
    'social_core.backends.facebook.FacebookAppOAuth2',
]

这样就有了三个验证后端,分别进行默认的用户名验证,电子邮件验证以及Facebook验证。

剩下的问题就是需要到Facebook上建立应用来让social-auth去访问,以便获取到Facebook的用户信息。按照如下操作:

  • 访问https://developers.facebook.com/apps/,新建一个应用
  • 弹出的表单里填写App的名称和联系邮箱,名称可以自定义,这里使用项目名称Bookmarks。点击右下角创建应用编号。之后会有reCAPTCHA验证。
  • 然后会到一个应用编号的管理页面中,点击添加产品中的 Facebook 登录 产品右下角的“设置”
  • 会到一个快速入门界面,有几个设置需要配置,第一个是选择平台,由于我们是站点,选择“WWW 网络”即可。
  • 之后的问题是网站网址,输入http://mysite.com:8000/并点击保存,然后点击继续。
  • 后边的其他设置用于在网页上直接显示通过Facebook登录,可以都跳过。
  • 点击左边导航栏的设置–基本,可以看到应用编号和应用密钥。
  • 将应用编号和应用密钥设置到settings.py里:
        SOCIAL_AUTH_FACEBOOK_KEY = '2178089959185509'
        SOCIAL_AUTH_FACEBOOK_SECRET = 'a7624ed2d32ed12311535f65396aa4'
  • 还可以配置向Facebook请求的额外信息比如用户邮件:
    SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
  • 在设置–基本里边的应用域名里填写 mysite.com,然后保存更改。
  • 点击左侧导航栏 产品—Facebook 登录—设置,确认以下内容已经打开:
    • OAuth 客户端授权登录
    • OAuth 网页授权登录
    • OAuth 嵌入式浏览器授权登录
  • 在“有效 OAuth 跳转 URI”中输入 http://mysite.com:8000/social-auth/complete/facebook/,然后保存更改。

Facebook的工作做完了,在login.html里的content内部追加这些内容:

    <div class="social">
    <ul>
        <li class="facebook"><a href="{% url 'social:begin' 'facebook' %}">Log in with Facebook</a></li>
    </ul>
    </div>

之后启动站点到登录页面,实验一下新出来的用Facebook登录的按钮。发现可以成功跳转到Facebook界面登录,只不过现在FB强制要求链接过来的网站使用HTTPS,结果本地的网站肯定无法继续调试了。

Twitter 第三方登录

先增加一个后端:

'social_core.backends.twitter.TwitterOAuth',

然后需要到https://apps.twitter.com/app/new进行一系列设置:

twitter的开发者账号还需要等待申请通过,所以这一部分先跳过。

Google 第三方登录

增加用于google验证的后端:

'social_core.backends.google.GoogleOAuth2',

然后同样是到google的开发者网站:https://console.developers.google.com/apis/credentials进行设置:

  • 选择左上角项目然后选新建项目,然后输入项目名称bookmarks,得到一个项目ID:bookmarks-217106,位置就默认无组织。
  • 然后选择该项目后,点击凭据,在右边的凭据中点击创建凭据,创建其中的OAuth客户端ID。
  • 在同意屏幕上设置产品名称。然后跳转到一个表单,邮件就用账号邮件,产品名称里输入Bookmarks,然后保存。之后跳转到一个个问题的页面。
  • 应用类型选择网页应用,名称输入Bookmarks;已获授权的重定向URI输入 http://mysite.com:8000/socialauth/complete/google-oauth2/,然后点击创建。
  • 然后google会弹出ID和密钥,在setting.py里设置:
        SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '908844356643-nf5g8eph820um55mqg7gb3ei0sg2r37n.apps.googleusercontent.com'
        SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'zVlyvjwcuvkhu5-ikH6FNr'
  • 左侧点API与服务,选择Social 里的 Google+ API 服务点击,然后再点击“启用”

Google的APP配置完成了,然后到login.html里加上跳转Google 的链接:

<li class="google"><a href="{% url 'social:begin' 'google-oauth2' %}">Log in with Google</a></li>

打开页面实验,现在就可以成功的通过Google登录了。

这里还一个小问题,就是通过第三方登录进来的用户,检查auth_user表会发现其实用户信息已经被写入到了该表里,但是Profile表没有对应生成该字段,导致这种方式登录的用户在修改字段的时候,会报错。

很多网站的做法是,通过第三方验证进来的用户,必须捆绑到通过本站注册账号中,也有的网站不加处理,直接当做新用户处理。这里我们简化一下处理,就是当用户修改字段的时候,去检测一下Profile表中该用户的外键是不是存在,如果不存在,就新建一个该外键的数据,然后再用这行返回表单实例供填写。修改后的edit视图如下:

@login_required
def edit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    else:
        try:
            Profile.objects.get(user=request.user)
        except Profile.DoesNotExist:
            Profile.objects.create(user=request.user)
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

其实还可以再完善一些,比如新进来用户的电子邮件如果与原来的电子邮件地址相同,那么就应该弹出提示或者实际上让用户作为原来的身份登录。很多网站也有类似的处理方法。

总结

这一章的核心就是内置的用户验证功能,以及围绕验证功能做的相关完善:

  • virtual env 建立开发环境
  • 两个中间件和auth模块构成的django内置用户验证功能
  • 一系列内置CBV和直接采用auth.url配置URL
  • 对应模板的默认存储位置和名称
  • APP里admin应用和自己的登录模块的前后关系,如果模板也使用内置的,配置一下url就可以完成用户验证功能。在这个项目里是把这个url配置到二级路由里去了。
  • 使用一对一表为用户增加额外用户信息
  • 消息模块和消息中间件的了解,所有模板中都存在messages变量。
  • 自定义验证后端类的方法要求
  • 第三方模块social-auth-app-django的引入和使用
  • Facebook,Twitter,Google的开发者API设置
  • settings.py里AUTHENTICATION_BACKENDS的设置以及social-auth设置第三方APP ID 和密钥