用户与Authentication

通过session,我们可以在多次浏览器请求中保持数据, 接下来的部分就是用session来处理用户登录了。 当然,不能仅凭用户的一面之词,我们就相信,所以我们需要认证。

当然了,Django 也提供了工具来处理这样的常见任务(就像其他常见任务一样)。 Django 用户认证系统处理用户帐号,组,权限以及基于cookie的用户会话。 这个系统一般被称为 auth/auth (认证与授权)系统。 这个系统的名称同时也表明了用户常见的两步处理。 我们需要

  • 验证 (认证) 用户是否是他所宣称的用户(一般通过查询数据库验证其用户名和密码)

  • 验证用户是否拥有执行某种操作的 授权 (通常会通过检查一个权限表来确认)

根据这些需求,Django 认证/授权 系统会包含以下的部分:

  • 用户 : 在网站注册的人

  • 权限 : 用于标识用户是否可以执行某种操作的二进制(yes/no)标志

  • :一种可以将标记和权限应用于多个用户的常用方法

  • Messages : 向用户显示队列式的系统消息的常用方法

如果你已经用了admin工具(详见第6章),就会看见这些工具的大部分。如果你在admin工具中编辑过用户或组,那么实际上你已经编辑过授权系统的数据库表了。

打开认证支持

像session工具一样,认证支持也是一个Django应用,放在 django.contrib 中,所以也需要安装。 与session系统相似,它也是缺省安装的,但如果它已经被删除了,通过以下步骤也能重新安装上:

  • 根据本章早前的部分确认已经安装了session 框架。 需要确认用户使用cookie,这样sesson 框架才能正常使用。

  • 'django.contrib.auth' 放在你的 INSTALLED_APPS 设置中,然后运行 manage.py syncdb以创建对应的数据库表。

  • 确认 SessionMiddleware 后面的 MIDDLEWARE_CLASSES 设置中包含 'django.contrib.auth.middleware.AuthenticationMiddleware' SessionMiddleware。

这样安装后,我们就可以在视图(view)的函数中处理user了。 在视图中存取users,主要用 request.user ;这个对象表示当前已登录的用户。 如果用户还没登录,这就是一个AnonymousUser对象(细节见下)。

你可以很容易地通过 is_authenticated() 方法来判断一个用户是否已经登录了:

  1. if request.user.is_authenticated():
  2. # Do something for authenticated users.
  3. else:
  4. # Do something for anonymous users.

使用User对象

User 实例一般从 request.user ,或是其他下面即将要讨论到的方法取得,它有很多属性和方法。 AnonymousUser 对象模拟了 部分 的接口,但不是全部,在把它当成真正的user对象 使用前,你得检查一下 user.is_authenticated() 表14-3和14-4分别列出了User 对象中的属性(fields)和方法。

表 14-3. User 对象属性
属性描述
username必需的,不能多于30个字符。 仅用字母数字式字符(字母、数字和下划线)。
first_name可选; 少于等于30字符。
last_name可选; 少于等于30字符。
email可选。 邮件地址。
password必需的。 密码的哈希值(Django不储存原始密码)。 See the Passwords section for more about this value.
is_staff布尔值。 用户是否拥有网站的管理权限。
is_active布尔值. 设置该账户是否可以登录。 把该标志位置为False而不是直接删除账户。
is_superuser布尔值 标识用户是否拥有所有权限,无需显式地权限分配定义。
last_login用户上次登录的时间日期。 它被默认设置为当前的日期/时间。
date_joined账号被创建的日期时间 当账号被创建时,它被默认设置为当前的日期/时间。

System Message: ERROR/3 (<string>, line 735)

Error parsing content block for the “table” directive: exactly one table expected.

  1. .. table:: 14-4\. ``User`` 对象方法
  2. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  3. |方法 |描述 |
  4. +=============================================================================================+======================================================================================================================================================+
  5. |``is_authenticated()`` |对于真实的User对象,总是返回\ `` True`` |
  6. | |这是一个分辨用户是否已被鉴证的方法。 它并不意味着任何权限,也不检查用户是否仍是活动的。 它仅说明此用户已被成功鉴证。 |
  7. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  8. |``is_anonymous()`` |对于\ `` AnonymousUser`` 对象返回\ `` True`` (对于真实的\ `` User`` 对象返回\ `` False`` )。 |
  9. | |总的来说,比起这个方法,你应该倾向于使用\ `` is_authenticated()`` 方法。 |
  10. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  11. |``get_full_name()`` |返回\ `` first_name`` 加上\ `` last_name`` ,中间插入一个空格。 |
  12. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  13. |``set_password(passwd)`` |设定用户密码为指定字符串(自动处理成哈希串)。 实际上没有保存\ ``User``\对象。 |
  14. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  15. |check_password(passwd) |如果指定的字符串与用户密码匹配则返回\ ``True``\。 比较时会使用密码哈希表。 |
  16. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  17. |``get_group_permissions()`` |返回一个用户通过其所属组获得的权限字符串列表。 |
  18. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  19. |``get_all_permissions()`` |返回一个用户通过其所属组以及自身权限所获得的权限字符串列表。 |
  20. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  21. |``has_perm(perm)`` |如果用户有指定的权限,则返回\ `` True`` ,此时\ `` perm`` 的格式是\ `` "package.codename"`` 。如果用户已不活动,此方法总是返回\ `` False`` |
  22. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  23. |has_perms(perm_list) |如果用户拥有\ * 全部* 的指定权限,则返回\ `` True`` 如果用户是不活动的,这个方法总是返回\ `` False`` |
  24. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  25. |``has_module_perms(app_label)`` |如果用户拥有给定的\ `` app_label`` 中的任何权限,则返回\ `` True`` 。如果用户已不活动,这个方法总是返回\ `` False`` |
  26. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  27. |get_and_delete_messages() |返回一个用户队列中的\ `` Message`` 对象列表,并从队列中将这些消息删除。 |
  28. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
  29. |``email_user(subj, msg)`` |向用户发送一封电子邮件。 这封电子邮件是从\ `` DEFAULT_FROM_EMAIL`` 设置的地址发送的。 你还可以传送一个第三参数:\ `` from_email`` ,以覆盖电邮中的发送地址。|
  30. +---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+

最后,User 对象有两个many-to-many属性。 groupspermissions 。正如其他的many-to-many属性使用的方法一样,User 对象可以获得它们相关的对象:

  1. # Set a user's groups:
  2. myuser.groups = group_list
  3. # Add a user to some groups:
  4. myuser.groups.add(group1, group2,...)
  5. # Remove a user from some groups:
  6. myuser.groups.remove(group1, group2,...)
  7. # Remove a user from all groups:
  8. myuser.groups.clear()
  9. # Permissions work the same way
  10. myuser.permissions = permission_list
  11. myuser.permissions.add(permission1, permission2, ...)
  12. myuser.permissions.remove(permission1, permission2, ...)
  13. myuser.permissions.clear()

登录和退出

Django 提供内置的视图(view)函数用于处理登录和退出 (以及其他奇技淫巧),但在开始前,我们来看看如何手工登录和退出。 Django提供两个函数来执行django.contrib.auth\中的动作 : authenticate()

login()

认证给出的用户名和密码,使用 authenticate() 函数。它接受两个参数,用户名 username 和 密码 password ,并在密码对给出的用户名合法的情况下返回一个 User 对象。 如果密码不合法,authenticate()返回None

  1. >>> from django.contrib import auth
  2. >>> user = auth.authenticate(username='john', password='secret')
  3. >>> if user is not None:
  4. ... print "Correct!"
  5. ... else:
  6. ... print "Invalid password."

authenticate() 只是验证一个用户的证书而已。 而要登录一个用户,使用 login() 。该函数接受一个 HttpRequest 对象和一个 User 对象作为参数并使用Django的会话( session )框架把用户的ID保存在该会话中。

下面的例子演示了如何在一个视图中同时使用 authenticate()login() 函数:

  1. from django.contrib import auth
  2. def login_view(request):
  3. username = request.POST.get('username', '')
  4. password = request.POST.get('password', '')
  5. user = auth.authenticate(username=username, password=password)
  6. if user is not None and user.is_active:
  7. # Correct password, and the user is marked "active"
  8. auth.login(request, user)
  9. # Redirect to a success page.
  10. return HttpResponseRedirect("/account/loggedin/")
  11. else:
  12. # Show an error page
  13. return HttpResponseRedirect("/account/invalid/")

注销一个用户,在你的视图中使用 django.contrib.auth.logout() 。 它接受一个HttpRequest对象并且没有返回值。

  1. from django.contrib import auth
  2. def logout_view(request):
  3. auth.logout(request)
  4. # Redirect to a success page.
  5. return HttpResponseRedirect("/account/loggedout/")

注意,即使用户没有登录, logout() 也不会抛出任何异常。

在实际中,你一般不需要自己写登录/登出的函数;认证系统提供了一系例视图用来处理登录和登出。 使用认证视图的第一步是把它们写在你的URLconf中。 你需要这样写:

  1. from django.contrib.auth.views import login, logout
  2. urlpatterns = patterns('',
  3. # existing patterns here...
  4. (r'^accounts/login/$', login),
  5. (r'^accounts/logout/$', logout),
  6. )

/accounts/login//accounts/logout/ 是Django提供的视图的默认URL。

缺省情况下, login 视图渲染 registragiton/login.html 模板(可以通过视图的额外参数 template_name 修改这个模板名称)。 这个表单必须包含 usernamepassword 域。如下示例: 一个简单的 template 看起来是这样的

  1. {% extends "base.html" %}
  2. {% block content %}
  3. {% if form.errors %}
  4. <p class="error">Sorry, that's not a valid username or password</p>
  5. {% endif %}
  6. <form action="" method="post">
  7. <label for="username">User name:</label>
  8. <input type="text" name="username" value="" id="username">
  9. <label for="password">Password:</label>
  10. <input type="password" name="password" value="" id="password">
  11. <input type="submit" value="login" />
  12. <input type="hidden" name="next" value="{{ next|escape }}" />
  13. </form>
  14. {% endblock %}

如果用户登录成功,缺省会重定向到 /accounts/profile 。 你可以提供一个保存登录后重定向URL的next隐藏域来重载它的行为。 也可以把值以GET参数的形式发送给视图函数,它会以变量next的形式保存在上下文中,这样你就可以把它用在隐藏域上了。

logout视图有一些不同。 默认情况下它渲染 registration/logged_out.html 模板(这个视图一般包含你已经成功退出的信息)。 视图中还可以包含一个参数 next_page 用于退出后重定向。

限制已登录用户的访问

有很多原因需要控制用户访问站点的某部分。

一个简单原始的限制方法是检查 request.user.is_authenticated() ,然后重定向到登陆页面:

  1. from django.http import HttpResponseRedirect
  2. def my_view(request):
  3. if not request.user.is_authenticated():
  4. return HttpResponseRedirect('/accounts/login/?next=%s' % request.path)
  5. # ...

或者显示一个出错信息:

  1. def my_view(request):
  2. if not request.user.is_authenticated():
  3. return render_to_response('myapp/login_error.html')
  4. # ...

作为一个快捷方式, 你可以使用便捷的 login_required 修饰符:

  1. from django.contrib.auth.decorators import login_required
  2. @login_required
  3. def my_view(request):
  4. # ...

login_required 做下面的事情:

  • 如果用户没有登录, 重定向到 /accounts/login/ , 把当前绝对URL作为 next 在查询字符串中传递过去, 例如: /accounts/login/?next=/polls/3/ 。

  • 如果用户已经登录, 正常地执行视图函数。 视图代码就可以假定用户已经登录了。

对通过测试的用户限制访问

限制访问可以基于某种权限,某些检查或者为login视图提供不同的位置,这些实现方式大致相同。

一般的方法是直接在视图的 request.user 上运行检查。 例如,下面视图确认用户登录并是否有 polls.can_vote权限:

  1. def vote(request):
  2. if request.user.is_authenticated() and request.user.has_perm('polls.can_vote')):
  3. # vote here
  4. else:
  5. return HttpResponse("You can't vote in this poll.")

并且Django有一个称为 user_passes_test 的简洁方式。它接受参数然后为你指定的情况生成装饰器。

  1. def user_can_vote(user):
  2. return user.is_authenticated() and user.has_perm("polls.can_vote")
  3. @user_passes_test(user_can_vote, login_url="/login/")
  4. def vote(request):
  5. # Code here can assume a logged-in user with the correct permission.
  6. ...

user_passes_test 使用一个必需的参数: 一个可调用的方法,当存在 User 对象并当此用户允许查看该页面时返回 True 。 注意 user_passes_test 不会自动检查 User

是否认证,你应该自己做这件事。

例子中我们也展示了第二个可选的参数 login_url ,它让你指定你的登录页面的URL(默认为 /accounts/login/ )。 如果用户没有通过测试,那么user_passes_test将把用户重定向到login_url

既然检查用户是否有一个特殊权限是相对常见的任务,Django为这种情形提供了一个捷径: permission_required() 装饰器。 使用这个装饰器,前面的例子可以改写为:

  1. from django.contrib.auth.decorators import permission_required
  2. @permission_required('polls.can_vote', login_url="/login/")
  3. def vote(request):
  4. # ...

注意, permission_required() 也有一个可选的 login_url 参数, 这个参数默认为 '/accounts/login/'

限制通用视图的访问

在Django用户邮件列表中问到最多的问题是关于对通用视图的限制性访问。 为实现这个功能,你需要自己包装视图,并且在URLconf中,将你自己的版本替换通用视图:

  1. from django.contrib.auth.decorators import login_required
  2. from django.views.generic.date_based import object_detail
  3. @login_required
  4. def limited_object_detail(*args, **kwargs):
  5. return object_detail(*args, **kwargs)

当然, 你可以用任何其他限定修饰符来替换 login_required

管理 Users, Permissions 和 Groups

管理认证系统最简单的方法是通过管理界面。 第六章讨论了怎样使用Django的管理界面来编辑用户和控制他们的权限和可访问性,并且大多数时间你使用这个界面就可以了。

然而,当你需要绝对的控制权的时候,有一些低层 API 需要深入专研,我们将在下面的章节中讨论它们。

创建用户

使用 create_user 辅助函数创建用户:

  1. >>> from django.contrib.auth.models import User
  2. >>> user = User.objects.create_user(username='john',
  3. ... email='jlennon@beatles.com',
  4. ... password='glass onion')

在这里, userUser 类的一个实例,准备用于向数据库中存储数据。(create_user()实际上没有调用save())。 create_user() 函数并没有在数据库中创建记录,在保存数据之前,你仍然可以继续修改它的属性值。

  1. >>> user.is_staff = True
  2. >>> user.save()

修改密码

你可以使用 set_password() 来修改密码:

  1. >>> user = User.objects.get(username='john')
  2. >>> user.set_password('goo goo goo joob')
  3. >>> user.save()

除非你清楚的知道自己在做什么,否则不要直接修改 password 属性。 其中保存的是密码的 加入salt的hash值 ,所以不能直接编辑。

一般来说, User 对象的 password 属性是一个字符串,格式如下:

  1. hashtype$salt$hash

这是哈希类型,salt和哈希本身,用美元符号($)分隔。

hashtypesha1 (默认)或者 md5 ,它是用来处理单向密码哈希的算法。 Salt是一个用来加密原始密码以创建哈希的随机字符串,例如:

  1. sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4

User.set_password()User.check_password() 函数在后台处理和检查这些值。

salt化得哈希值

一次 哈希 是一次单向的加密过程,你能容易地计算出一个给定值的哈希码,但是几乎不可能从一个哈希码解出它的原值。

如果我们以普通文本存储密码,任何能进入数据库的人都能轻易的获取每个人的密码。 使用哈希方式来存储密码相应的减少了数据库泄露密码的可能。

然而,攻击者仍然可以使用 暴力破解 使用上百万个密码与存储的值对比来获取数据库密码。 这需要花一些时间,但是智能电脑惊人的速度超出了你的想象。

更糟糕的是我们可以公开地得到 rainbow tables (一种暴力密码破解表)或预备有上百万哈希密码值的数据库。 使用rainbow tables可以在几秒之内就能搞定最复杂的一个密码。

在存储的hash值的基础上,加入 salt 值(一个随机值),增加了密码的强度,使得破解更加困难。 因为每个密码的salt值都不相同,这也限制了rainbow table的使用,使得攻击者只能使用最原始的暴力破解方法。

加入salt值得hash并不是绝对安全的存储密码的方法,然而却是安全和方便之间很好的折衷。

处理注册

我们可以使用这些底层工具来创建允许用户注册的视图。 最近每个开发人员都希望实现各自不同的注册方法,所以Django把写注册视图的工作留给了你。 幸运的是,这很容易。

作为这个事情的最简化处理, 我们可以提供一个小视图, 提示一些必须的用户信息并创建这些用户。 Django为此提供了可用的内置表单, 下面这个例子就使用了这个表单:

  1. from django import forms
  2. from django.contrib.auth.forms import UserCreationForm
  3. from django.http import HttpResponseRedirect
  4. from django.shortcuts import render_to_response
  5. def register(request):
  6. if request.method == 'POST':
  7. form = UserCreationForm(request.POST)
  8. if form.is_valid():
  9. new_user = form.save()
  10. return HttpResponseRedirect("/books/")
  11. else:
  12. form = UserCreationForm()
  13. return render_to_response("registration/register.html", {
  14. 'form': form,
  15. })

这个表单需要一个叫 registration/register.html 的模板。这个模板可能是这样的:

  1. {% extends "base.html" %}
  2. {% block title %}Create an account{% endblock %}
  3. {% block content %}
  4. <h1>Create an account</h1>
  5. <form action="" method="post">
  6. {{ form.as_p }}
  7. <input type="submit" value="Create the account">
  8. </form>
  9. {% endblock %}

在模板中使用认证数据

当前登入的用户以及他(她)的权限可以通过 RequestContext 在模板的context中使用(详见第9章)。

注意

从技术上来说,只有当你使用了 RequestContext这些变量才可用。 _并且_TEMPLATE_CONTEXT_PROCESSORS 设置包含了 “django.core.context_processors.auth” (默认情况就是如此)时,这些变量才能在模板context中使用。 TEMPLATE_CONTEXT_PROCESSORS 设置包含了 "django.core.context_processors.auth" (默认情况就是如此)时,这些变量才能在模板context中使用。

当使用 RequestContext 时, 当前用户 (是一个 User 实例或一个 AnonymousUser 实例) 存储在模板变量 {{ user }} 中:

  1. {% if user.is_authenticated %}
  2. <p>Welcome, {{ user.username }}. Thanks for logging in.</p>
  3. {% else %}
  4. <p>Welcome, new user. Please log in.</p>
  5. {% endif %}

这些用户的权限信息存储在 {{ perms }} 模板变量中。

你有两种方式来使用 perms 对象。 你可以使用类似于 {{ perms.polls }} 的形式来检查,对于某个特定的应用,一个用户是否具有 任意 权限;你也可以使用 {{ perms.polls.can_vote }} 这样的形式,来检查一个用户是否拥有特定的权限。

这样你就可以在模板中的 {% if %} 语句中检查权限:

  1. {% if perms.polls %}
  2. <p>You have permission to do something in the polls app.</p>
  3. {% if perms.polls.can_vote %}
  4. <p>You can vote!</p>
  5. {% endif %}
  6. {% else %}
  7. <p>You don't have permission to do anything in the polls app.</p>
  8. {% endif %}