几乎所有的网站都提供了用户注册与管理功能,这一节,我们将讲解如何利用Django自身提供的用户认证系统实现用户注册与管理功能。
会话认证
在上一节中,我们学习了User数据模型,并用它来保存用户信息,实际上用户数据模型只是Django提供的认证管理系统的一小部分,Django认证系统位于django.contrib.auth包中,默认情况下是已经安装了的。
可以通过检查settings.py文件中的INSTALLED_APPS来判断是否已经安装了,它的配置信息应该如下:
INSTALLED_APPS = ( ‘django.contrib.auth‘, ‘django.contrib.contenttypes‘, ‘django.contrib.sessions‘, ‘django.contrib.sites‘, ‘django_bookmarks.bookmarks‘, )
让我们先了解下Django认证系统都提供了哪些功能:
- User:用户数据模型
- Permissions:权限控制
- Groups:组管理
- Messages:用户信息
创建登录页面
如果我们检查上一节的User数据模型,会发现其实里面已经包含了一个用户,以就是创建项目时创建的root用户。
相比那些直接在底层操作session信息的的web开发语言(那样更加麻烦,稍不留神就可能导致安全问题),如PHP,Django非常小心的为我们实现了session管理功能,激活它只需要将其暴露给一些执行的视图函数,我们不需要担心用户会话信息的管理与口令认证的过程,Django已经为我们实现了一切。
为了创建登录页面,首先需要编辑urls.py,添加下面这条URL规则:
urlpatterns = patterns(‘‘, (r‘^$‘, main_page), (r‘^user/(\w+)/$‘, user_page), (r‘^login/$‘, ‘django.contrib.auth.views.login‘), )
这个URL跟之前不太一样,之前的都是使用视图函数,这里我们直接使用模块路径作为参数,这是django提供的一个快捷方式,通常用于从当前项目之外模块中导入视图,Django会自动导入相应视图。
django.contrib.auth.views提供了一些与会话管理相关的视图函数,这里我们只使用login视图,这个视图处理用户的登录,但是在使用它之前,我们还需要编写一个模板。
使用login视图需要在提供一个模板,这个模板的路径为registration/login.html,login视图会加载这个模板然后传递一个login表单对象给它,在后面我们将详细学习表单对象,但是现在我们只需要知道这个表单对象form有如下属性form.username, form.password以及form.has_errors。
接下来创建模板,在templates目录下新建一个registration文件夹,然后创建一个文件login.html:
<html> <head> <title>Django Bookmarks - User Login</title> </head> <body> <h1>User Login</h1> {% if form.has_errors %} <p>Your username and password didn‘t match. Please try again.</p> {% endif %} <form method="post" action="."> <p><label for="id_username">Username:</label> {{ form.username }}</p> <p><label for="id_password">Password:</label> {{ form.password }}</p> <input type="hidden" name="next" value="/" /> {% csrf_token %} <input type="submit" value="login" /> </form> </body> </html>
这段代码中首先检查是否有登陆错误,然后创建了一个包含用户与口令以及提交按钮的表单,表单中还有一个隐藏按钮,name为next,值为成功登录之后跳转的URL。在表单中还有一个{% csrf_tag %}标签,这个是用来防止跨域非法访问的,在使用post方法提交这个表单的时候必须使用这个标签。
接下来就可以进行登录了,输入http://127.0.0.1:8000/login/即可。可以使用刚开始创建项目时创建的用户进行登录。成功登录之后就会重定向至主页。既然我们可以登录了,那么在主页中就应该显示我们是否处于已登录状态。对templates/main_page.html进行修改:
<html> <head> <title>Django Bookmarks</title> </head> <body> <h1>Welcome to Django Bookmarks</h1> {% if user.username %} <p>Welcome {{ user.username }}! Here you can store and share bookmarks!</p> {% else %} <p>Welcome anonymous user! You need to <a href="/login/">login</a> before you can store and share bookmarks.</p> {% endif %} </body> </html>
现在这个模板可以通过检查变量user.username是否已经赋值,如果有,则显示欢迎信息,否则,显示登录的链接。这个显示渲染成功的前提是user对象传递给了它,所以需要修改bookmarks/views.py:
def main_page(request): template = get_template(‘main_page.html‘) variables = Context({ ‘user‘: request.user }) output = template.render(variables) return HttpResponse(output)
user对象从request中获取,然后传递给模板进行渲染。刷新主页,,就可以显示友好的欢迎信息了。
你可能发现,加载模板,传递变量,渲染页面是个经常性的工作,那有没有简便的方法了,这些Django都想到了,所以Django提供了render_to_response,修改代码/bookmarks/views.py:
from django.shortcuts import render_to_response def main_page(request): return render_to_response( ‘main_page.html‘, { ‘user‘: request.user } )
这样一来方便多了,只需要引入一个模块,使用一条语句就完成了上面的三个工作。
request中的user对象实际上就是我们之前介绍的User对象模型实例,我们已经熟悉了它的常用属性,比如用户名,密码等,接下来再了解一下它的一些常用方法:
- is_authenticated() 返回一个布尔值,判断用户是否登录
- get_full_name() 获取用户全名
- email_user(subject, message, from_email=None) 发送一封邮件给用户
- set_password(raw_password) 设置用户密码
- check_password(raw_password) 检查用户密码是否正确,并返回一个布尔值。
既然用户的口令很容易获取,为什么上面还有个set_password()方法呢?我们先看下面的例子:
>>> from django.contrib.auth.models import User >>> user = User.objects.get(id=1) >>> user.password ‘sha1$e1f02$bc3c0ef7d3e5e405cbaac0a44cb007c3d34c372c‘
从上面可以得知,返回的password是个很长的字符串,而不是原始的密码,这其中发生了什么呢?这是处于安全考虑,Django不会直接保存原始密码,而是保存密码的hash值,这个hash值很难逆向获取,但是依然可以用来验证密码。所以设置密码必须调用set_password()。
实现注销功能
实现登录功能之后,下一步就是实现注销功能了,当用户单击/logout时,就注销用户然后重定向至主页。首先在bookmarks/views.py添加下面的视图函数:
from django.http import HttpResponseRedirect from django.contrib.auth import logout def logout_page(request): logout(request) return HttpResponseRedirect(‘/‘)
我们使用django.contrib.auth中提供的logout函数,删除相应用户的会话信息,然后重定向至主页,这里使用django.http提供的HttpRequestRedirect对象。
添加完视图函数之后,就需要提供URL入口,所以编辑urls.py:
urlpatterns = patterns(‘‘, (r‘^$‘, main_page), (r‘^user/(\w+)/$‘, user_page), (r‘^login/$‘, ‘django.contrib.auth.views.login‘), (r‘^logout/$‘, logout_page), )
为了让注销的链接对用户可见,我们需要对所有的模板进行编辑,这是很不现实的,因为改动的范围比较大,为了克服这个缺点,我们接下来将学习模板继承的使用。
改进模板结构
到现在我们已经创建了三个模板,它们都有着相似的结构,只是标题与内容不同而已,那么我们可不可以提取出这些模板的共性保存在一个文件中,然后再在这个基础上进行继承修改呢?答案是可以的。
Django提供的模板系统已经支持这样的模板继承,原理很简单,首先创建一个基础模板base.html,基础模板中包含子模板中需要修改的代码块block,然后创建一个继承于基础模板的子模板,
在templates目录下创建base.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Django Bookmarks | {% block title %}{% endblock %}</title> </head> <body> <h1>{% block head %}{% endblock %}</h1> {% block content %}{% endblock %} </body> </html>
这个模板中使用了一个新的标签block,它用来定义子模板中可以修改的部分,base模板中包含了三个block,分别是title,head以及content。
接下来修改/templates/main_page.html,将它的内容替换成:
{% extends "base.html" %} {% block title %}Welcome to Django Bookmarks{% endblock %} {% block head %}Welcome to Django Bookmarks{% endblock %} {% block content %} {% if user.username %} <p>Welcome {{ user.username }}! Here you can store and share bookmarks!</p> {% else %} <p>Welcome anonymous user! You need to <a href="/login/">login</a> before you can store and share bookmarks.</p> {% endif %} {% endblock %}
新模板首先使用extend继承base.html模板,也就是说main_page.html是base.html的子模板,main_page.html不再包含通用的HTML的结构,它只需要重新定义基础模板中需要修改的部分。
接下里修改/templates/user_page.html:
{% extends "base.html" %} {% block title %}{{ username }}{% endblock %} {% block head %}Bookmarks for {{ username }}{% endblock %} {% block content %} {% if bookmarks %} <ul> {% for bookmark in bookmarks %} <li><a href="{{ bookmark.link.url }}"> {{ bookmark.title }}</a></li> {% endfor %} </ul> {% else %} <p>No bookmarks found.</p> {% endif %} {% endblock %}
最后修改/templates/registration/login.html:{% extends "base.html" %}:
{% extends "base.html" %} {% block title %}User Login{% endblock %} {% block head %}User Login{% endblock %} {% block content %} {% if form.has_errors %} <p>Your username and password didn‘t match. Please try again.</p> {% endif %} <form method="post" action="."> <p><label for="id_username">Username:</label> {{ form.username }}</p> <p><label for="id_password">Password:</label> {{ form.password }}</p> {% csrf_token %} <input type="submit" value="login" /> <input type="hidden" name="next" value="/" /> </form> {% endblock %}
现在我们的模板有了一个通用的基础模板,接下来我们开始优化站点的可用性以及外观。我们可以给项目增加CSS文件,CSS以及图像都是静态文件。在产品部署时通过web服务器提供支持,但是在Django开发环境中,则需要进行下面的操作才行。
打开urls.py,进行如下修改:
import os.path from django.conf.urls.defaults import * from bookmarks.views import * site_media = os.path.join( os.path.dirname(__file__), ‘static‘ ) urlpatterns = patterns(‘‘, (r‘^$‘, main_page), (r‘^user/(\w+)/$‘, user_page), (r‘^login/$‘, ‘django.contrib.auth.views.login‘), (r‘^logout/$‘, logout_page), (r‘^site_media/(?P<path>.*)$‘, ‘django.views.static.serve‘, { ‘document_root‘: site_media }), )
在settings.py中的STATICFILES_DIRS添加os.path.join(os.path.dirname(__file__), ‘static‘)。
在项目中新建一个文件夹site_media,在其中新建style.css,然后编辑templates/base.html,添加css的链接 并增加导航菜单:
<html> <head> <title>Django Bookmarks | {% block title %}{% endblock %}</title> <link rel="stylesheet" href="/site_media/style.css" type="text/css" /> </head> <body> <div id="nav"> <a href="/">home</a> | {% if user.is_authenticated %} welcome {{ user.username }} (<a href="/logout">logout</a>) {% else %} <a href="/login/">login</a> {% endif %} </div> <h1>{% block head %}{% endblock %}</h1> {% block content %}{% endblock %} </body> </html>
编辑style.css,使导航菜单靠近页面右侧:
#nav { float: right; }
现在主页中就可以正常显示导航菜单了,但是如果打开用户页面,却发现不管用户有没有登录,导航菜单中都会显示登录链接,这是因为导航菜单中的if标签使用user对象来检查用户的状态,但是这个对象并没有通过Context变量传递给user_page.html模板,为了克服这个问题,可以使用下面两种办法:
- 修改user_page视图函数,将user对象传递给模板。
- 使用RequestContext对象,这个对象与Context对象稍微不同,它会自动将user对象以及其他几个变量传递给模板。为了实现这个目的,RequestContext使用request变量作为第一个参数,一个模板变量组成的字典作为第二个参数。
我们将使用第二个方法,因为它更加简洁,因为我们需要将user对象传递给每个模板,而且,这样可以提炼出通用代码,减少代码编写量。
使用RequestContext修改main_page与user_page,编辑bookmarks/views.py,定位到main_page视图,进行如下修改:
from django.template import RequestContext def main_page(request): return render_to_response( ‘main_page.html‘, RequestContext(request) )
从上面可以看出,我们不需要再传递request.user变量给Context对象了,RequestContext自动进行处理。修改user_page:
def user_page(request, username): try: user = User.objects.get(username=username) except: raise Http404(‘Requested user not found.‘) bookmarks = user.bookmark_set.all() variables = RequestContext(request, { ‘username‘: username, ‘bookmarks‘: bookmarks }) return render_to_response(‘user_page.html‘, variables)
这样代码看起来更加紧凑了,尽管重构模板花费了不少时间,但是新的结构在以后会给我们节省下大量时间。
用户注册
第一个用户是在创建Django项目时生成的,但是网站也需要为访客提供注册功能,在现在的社交网站中,用户注册是很基本的功能,这一节我们将学习如何实现用户注册功能。
Django表单
创建、验证并处理表单数据是一个很普遍的操作,Web应用接收用户输入并通过表单的形式手机数据,所以很自然的Django提供了自带的模块forms实现这些通用功能,使用下面的命令导入模块:
from django import forms
Django表单库可以完成下面三个任务:
- 生成表单
- 服务器端表单验证
- 表单错误显示
表单form工作的模式与数据模型models类似,首先定义一个表单类,这个类必须继承forms.Form基类,类属性代表表单元素,form模块提供了很多表单类型,跟models类似。除此之外,form还提供了很多方法,比如生成HTML代码,获取表单数据,验证表单等等。
设计用户注册表单
让我们来创建第一个Django表单,在bookmarks中创建forms.py,键入如下代码:
from django import forms class RegistrationForm(forms.Form): username = forms.CharField(label=‘Username‘, max_length=30) email = forms.EmailField(label=‘Email‘) password1 = forms.CharField( label=‘Password‘, widget=forms.PasswordInput() ) password2 = forms.CharField( label=‘Password (Again)‘, widget=forms.PasswordInput() )
定义Registration,它继承于forms.Form,所有表单类都继承于这个基类,在forms模块中有许多字段类型,还有其它参数,如下所示:
- label:当生成HTML代码的时候表单元素的lable名
- required:表单元素是否必填,默认为True
- widget:控制字段的渲染结果,也就是表单元素的类型
- help_text:帮助文本
下面是常见的字段类型:
字段类型 描述
CharField 返回一个字符串
IntegerField 返回一个整数
DateField 返回一个Python datetime.date 对象
DateTimeField 返回一个Python datetime.datetime对象
EmailField 返回一个合法的email字符串
URLField 返回一个合法的URL字符串
下面是常用表单元素类型:
表单元素类型 描述
PasswordInput 密码输入框
HiddenInput 隐藏输入框
Textarea 多行文本输入框
FileInput 文件上传框
我们可以通过交互命令窗口了解更多关于forms的API:
$ python manage.py shell >>> from bookmarks.forms import * >>> form = RegistrationForm()
接下来将表单渲染成HTML:
>>> print form.as_table(
结果将输出表格形式的表单,除此之外,还可以通过form.as_ul()以及form.as_p()分别将表单渲染成列表形式以及段落形式。
此外,我们还可以直接渲染单个的表单元素:
>>> print form[‘username‘] <input id="id_username" type="text" name="username" maxlength="30" />
现在我们已经知道如何渲染表单,接下来我们将学习如何验证表单:
>>> form = RegistrationForm({ ... ‘username‘: ‘test‘, ... ‘email‘: ‘[email protected]‘, ... ‘password1‘: ‘test‘, ... ‘password2‘: ‘test‘ ... }) >>> form.is_valid() True
form.is_valid()返回True,因为所有的表单元素都通过了验证,接下来使用下面的非法表单:
>>> form = RegistrationForm({ ... ‘username‘: ‘test‘, ... ‘email‘: ‘invalid email‘, ... ‘password1‘: ‘test‘, ... ‘password2‘: ‘test‘ ... }) >>> form.is_valid() False >>> form.errors {‘email‘: [u‘Enter a valid e-mail address.‘]}
Django为我们处理了表单验证。我们可以使用form.is_bound来检查表单是否有数据,如果你尝试验证一个没有数据的表单,结果将抛出异常,用户输入可以通过form.data获取,如果表单通过验证,合法的输入就可以通过form.cleaned_data获取。
熟悉表单验证之后,就可以改进之前的表单代码,使其具有表单验证功能:
- 防止用户输入不合法的用户名或者已经存在的用户名
- 确保两次输入的密码一致。
首先修改密码验证,打开bookmarks/forms.py,给Registration类添加如下方法:
def clean_password2(self): if ‘password1‘ in self.cleaned_data: password1 = self.cleaned_data[‘password1‘] password2 = self.cleaned_data[‘password2‘] if password1 == password2: return password2 raise forms.ValidationError(‘Passwords do not match.‘)
下面分析下这段代码:
- 方法名为clean_password2,自定义的验证方法都遵循这个格式clean_fieldname。
- 首先检查password1是否通过验证,如果通过验证,那么通过self.cleaned_data字典就可以获取到。
- 接下来检查两次输入的密码是否一致,如果一致则返回正确的password2。
- 如果验证失败,则抛出forms.ValidationError异常。
实现密码验证之后,接着实现用户名验证,在bookmarks/forms.py中添加如下语句:
import re from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist
re是正则表达式模块,我们使用它来确保用户名不包含非法字符,此外,我们还导入User数据模型,以检查输入的用户名是否已经存在,最后,引入ObjectNotExist异常,以便在找不到相应对象时抛出:
对RegistrationForm进行修改,添加如下方法:
def clean_username(self): username = self.cleaned_data[‘username‘] if not re.search(r‘^\w+$‘, username): raise forms.ValidationError(‘Username can only contain alphanumeric characters and the underscore.‘) try: User.objects.get(username=username) except ObjectDoesNotExist: return username raise forms.ValidationError(‘Username is already taken.‘)
首先检查用户名是否包含非法字符串,然后减产这个用户名是否已经被使用。我们可以通过交互命令行来检查上面的验证方法。如果你愿意的话,也可以给email添加验证方法。
现在注册表单已经准备好了,但是还需要视图函数与模板,我们先创建视图函数,编辑bookmarks/views.py,键入如下代码:
from bookmarks.forms import * def register_page(request): if request.method == ‘POST‘: form = RegistrationForm(request.POST) if form.is_valid(): user = User.objects.create_user( username=form.cleaned_data[‘username‘], password=form.cleaned_data[‘password1‘], email=form.cleaned_data[‘email‘] ) return HttpResponseRedirect(‘/‘) else: form = RegistrationForm() variables = RequestContext(request, { ‘form‘: form }) return render_to_response( ‘registration/register.html‘, variables )
如果使用POST方法发送请求,用户就提交注册信息,视图函数处理用户输入信息,然后重定向至主页面,如果是其他请求,则生成注册表单,然后渲染模板registration/register.html。
我们从request.POST中获取用户输入,request.POST是一个字典,它包含了通过POST方法提交的数据,我们直接将这个字典传递给RegistrationForm构造函数,然后就可以进行表单验证了。
你可能注意到这里是使用User.object.create_user()方法来创建用户,而不是使用实例化User类的形式,我们之所以使用这个方法是因为create_user能帮我们处理密码的哈希化,还有其他一些操作。
如果验证失败,表单对象还是会传递给模板但是表单对象可以提供有用的错误信息给用户。
接下来创建模板,新建一个文件templates/registration/register.html,然后田间如下代码:
{% extends "base.html" %} {% block title %}User Registration{% endblock %} {% block head %}User Registration{% endblock %} {% block content %} <form method="post" action="."> {{ form.as_p }} {% csrf_token %} <input type="submit" value="register" /> </form> {% endblock %}
现在发现视图与模板的紧凑与直接没,这些都是Django强大的表单库的功劳。
最后添加url,编辑urls.py,添加如下代码:
(r‘^register/$‘, register_page),
这样就大功告成了!打开页面一看,怎么感觉有点凌乱,为了让它看起来正常点,我们给它增加一点样式,编辑style.css:
input { display: block; }
现在看起来就正常多了,多测试几遍,看看输入正确信息与不正确信息页面将如何展示。
注册用户页面实现之后,我们还需要给导航菜单添加注册链接,编辑templates/base.html:
<div id="nav"> <a href="/">home</a> | {% if user.is_authenticated %} welcome {{ user.username }} (<a href="/logout">logout</a>) {% else %} <a href="/login/">login</a> | <a href="/register/">register</a> {% endif %} </div>
还有一件事没做,那就是在用户成功提交注册信息之后,展示一个注册成功的信息,实现这个功能非常简单,只需要加载并展示一个模板即可,Django已经为我们提供了这样一个视图,那就是django.views.generic.simple中的direct_to_templates,我们可以直接使用它。
创建模板templates/registration/register_success.html:
{% extends "base.html" %} {% block title %}Registration Successful{% endblock %} {% block head %} Registration Completed Successfully {% endblock %} {% block content %} Thank you for registering. Your information has been saved in the database. Now you can either <a href="/login/">login</a> or go back to the <a href="/">main page</a>. {% endblock %}
然后编辑urls.py在开头导入下面的方法:
from django.views.generic.simple import direct_to_template
然后添加下面的url:
(r‘^register/success/$‘, direct_to_template, { ‘template‘: ‘registration/register_success.html‘ }),
这里模板名以字典的形式作为第三个参数传递给urlpattern,最后修改bookmarks/views.py中的register_view,在注册成功之后重定向至上面的模板,将下面的代码:
return HttpResponseRedirect(‘/‘)
改成:
return HttpResponseRedirect(‘/register/success/‘)
这样成功注册之后就会显示注册成功的页面了。
账户管理
现在我们已经实现了会话管理类与用户注册功能,但是我们还可以让用户能够更新账户信息,比如密码或者邮件地址,实现这样的功能,可以这样做:
- 方法一,使用Django提供的通用账号管理视图
- 方法二,设计自己的表单与视图函数。
每种方法都有各自的优缺点,设计自己的表单给予自己更多的控制权,但是需要更多的编码,而使用Django提供的视图,则更为简单,更快捷,但是这样一来就只能使用Django提供的表单了。
我来总结一下django.contrib.auth提供的通用视图,每个视图函数都需要相应的提供一个模板,并传递一些变量给这些模板,用户的输入由这些视图函数处理,不需要我们关心。所有的视图都位于django.contrib.auth.views当中:
- logout 注销用户,注销成功之后返回一个模板
- logout_then_login 注销用户,然后重定向至登录页面
- password_change 允许用户修改密码
- password_change_done 修改完密码之后,展示一个模板
- password_reset 允许用户重置密码,都是邮件接收新密码。
- password_reset_done,同上,但是返回一个页面
- redirect_to_login 重定向至登录页面
这些视图的用户与开始讲的login视图类似,更多信息可以查阅Django官方文档。通过这篇的学习,我们可以对Django认证系统以及会话机制有了一个了解。在我们以后自己的项目中就可以学以致用了。