第十二章 会话、用户和注册

优质
小牛编辑
118浏览
2023-12-01

是时候承认了:我们有意的避开了 web 开发中极其重要的方面。到目前为止,我们都在假定,网站流量是大量的匿名用户带来的。

这当然不对,浏览器的背后都是活生生的人(至少某些时候是)。我们忽略了一件重要的事情:互联网服务于人而不是机器。要开发一个真正令人心动的网站,我们必须面对浏览器后面活生生的人。

很不幸,这并不容易。HTTP 被设计为”无状态”,每次请求都处于相同的空间中。在一次请求和下一次请求之间没有任何状态保持,我们无法根据请求的任何方面(IP 地址,用户代理等)来识别来自同一人的连续请求。

在本章中你将学会如何搞定状态的问题。好了,我们会从较低的层次(cookies)开始,然后过渡到用高层的工具来搞定会话,用户和注册的问题。

Cookies

浏览器的开发者在很早的时候就已经意识到, HTTP’s 的无状态会对 Web 开发者带来很大的问题,于是(cookies)应运而生。cookies 是浏览器为 Web 服务器存储的一小段信息。每次浏览器从某个服务器请求页面时,它向服务器回送之前收到的 cookies

来看看它是怎么工作的。当你打开浏览器并访问 google.com ,你的浏览器会给 Google 发送一个 HTTP 请求,起始部分就象这样:

GET / HTTP/1.1

Host: google.com

...

当 Google 响应时,HTTP 的响应是这样的: HTTP/1.1 200 OK

Content-Type: text/html

Set-Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671;

expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com

Server: GWS/2.1

...

注意 Set-Cookie 的头部。你的浏览器会存储 cookie 值

( PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671 ) ,而且每次访问 google 站点都会回送这个 cookie 值。因此当你下次访问 Google 时,你的浏览器会发送像这样的请求:

GET / HTTP/1.1

Host: google.com

Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671

...

于是 Cookies 的值会告诉 Google,你就是早些时候访问过 Google 网站的人。这个值可能是数据库中存储用户信息的 key,可以用它在页面上显示你的用户名。

存取 Cookies

在 Django 中处理持久化,大部分时候你会更愿意用高层些的 session 和/或 后面要讨论的

user 框架。但在此之前,我们需要停下来在底层看看如何读写 cookies。这会帮助你理解本章节后面要讨论的工具是如何工作的,而且如果你需要自己操作 cookies,这也会有所帮助。

读取已经设置好的 cookies 极其简单,每个 request 对象都有一个 COOKIES 对象,可以象使用字典般使用它,你可以读取任何浏览器发给视图(view)的任何 cookies:

def show_color(request):

if "favorite_color" in request.COOKIES:

return HttpResponse("Your favorite color is %s" % \ request.COOKIES["favorite_color"])

else:

return HttpResponse("You don't have a favorite color.")

写 cookies 稍微复杂点,需要用 HttpResponse 对象的 set_cookie() 方法来写。这儿有个基于 GET 参数来设置 favorite_color cookie 的例子:

def set_color(request):

if "favorite_color" in request.GET:

# Create an HttpResponse object...

response = HttpResponse("Your favorite color is now %s" % \ request.GET["favorite_color"])

# ... and set a cookie on the response response.set_cookie("favorite_color",

request.GET["favorite_color"])

return response else:

return HttpResponse("You didn't give a favorite color.")

你可以给 response.set_cookie() 传递一些可选的参数来控制 cookie 的行为,详见表 12-1。

表 12-1: Cookie 选项

参数

缺省

描述

max_age

None

cookies 的持续有效时间(以秒计),如果设置为 None cookies 在浏览器关闭的时候就失效

了。

expires

None

cookies 的过期时间,格式: "Wdy, DD-Mth-YY HH:MM:SS GMT" 如果设置这个参数,它将覆盖 max_age 参数。

path

"/"

cookie 生效的路径前缀,浏览器只会把 cookie 回传给带有该路径的页 面,这样你可以避免将 cookie 传给站点中的其他的应用。

当你的应用不处于站点顶层的时候,这个参数会非常有用。

domain

None

cookie 生效的站点。你可用这个参数来构造一个跨站 cookie。如,

domain=".example.com" 所构造的 cookie 对下面这些站点都是可 读的:

www.example.com 、 www2.example.com 和

an.other.sub.domain.example.com 。

如果该参数设置为 None ,cookie 只能由设置它的站点读取。

secure

False

如果设置为 True ,浏览器将通过 HTTPS 来回传 cookie。

好坏参半的 Cookies

也许你已经注意到了,cookies 的工作方式可能导致的问题,一起来看看其中一些重要的方面:

cookies 存取完全是非强制性的,浏览器不保证这一点。事实上,所有的浏览器都让用户自己控制 是否接受 cookies。如果你想知道 cookies 对于 web 应用有多重要,你可以试着打开这个浏览器的 选项:提示我接受每次 cookie。

尽管 cookies 广为使用,但仍被认为是不可靠的的。这意味着,开发者使用 cookies 之前必须 检查用户是否可以接收 cookie。

更重要的是,*永远* 也不要在 cookie 中存储重要的数据。开发者在 cookie 中存储了不可恢复 的数据,而浏览器却因为某种原因将 cookie 中的数据清得一干二净,这样令人发指的故事在 Web 世界中比比皆是。

Cookie(特别是那些没通过 HTTPS 传输的)是非常不安全的。因为 HTTP 数据是以明文发送的,所以 特别容易受到嗅探攻击。也就是说,嗅探攻击者可以在网络中拦截并读取 cookies,因此你要 绝对避免在 cookies 中存储敏感信息。

还有一种被称为”中间人”的攻击更阴险,攻击者拦截一个 cookie 并将其用于另一个用户。第 19 章将深入讨论这种攻击的本质以及如何避免。

即使从预想中的接收者返回的 cookie 也是不安全的,因为大多数浏览器都提供了很方便的方法来 修改 cookies 的内容,有技术背景的用户甚至可以用像 mechanize

( http://wwwsearch.sourceforge.net/mechanize/ ) 这样的工具来手工构造 HTTP 请求。

因此不能在 cookies 中存储可能会被篡改的敏感数据,“经典”错误是:在 cookies 中存储 IsLoggedIn=1 ,以标识用户已经登录。犯这类错误的站点数量多的令人难以置信; 绕过这些网站的安全系统也是易如反掌。

Django 的 Session 框架

由于存在的限制与安全漏洞,cookies 和持续性会话已经成为 Web 开发中令人头疼的典范。好消息是,Django 的目标正是高效的“头疼杀手”,它自带的 session 框架会帮你搞定这些问题。

你可以用 session 框架来存取每个访问者任意数据,这些数据在服务器端存储,并用通过

cookie 来传输数据摘要。cookies 只存储数据的哈希会话 ID,而不是数据本身,从而避免了大部分的常见 cookie 问题。

下面我们来看看如何打开 session 功能,并在视图中使用它。

打开 Sessions 功能

Sessions 功能是通过一个中间件(middleware)和一个模型(model)来实现的。要打开

sessions 功能,需要以下几步操作:

  1. 编辑 MIDDLEWARE_CLASSES 配置,确保 MIDDLEWARE_CLASSES 中包含 'django.contrib.sessions.middleware.SessionMiddleware'

  2. 确认 INSTALLED_APPS 中有 'django.contrib.sessions' (如果你是刚打开这个应用,别忘了运行 manage.py syncdb )

如果项目是用 startproject 来创建的,配置文件中都已经安装了这些东西,除非你自己删除,正常情况下,你无需任何设置就可以使用 session 功能。

如果不需要 session 功能,你可以删除 MIDDLEWARE_CLASSES 设置中的 SessionMiddleware和 INSTALLED_APPS 设置中的 'django.contrib.sessions' 。虽然这只会节省很少的开销,但积少成多啊。

在视图中使用 Session

SessionMiddleware 激活后,每个传给视图(view)函数的 HttpRequest 对象的第一个参数都有一个 session 属性,这是一个字典型的对象。你可以象用普通字典一样来用它。例如,在视图(view)中你可以这样用:

# Set a session value: request.session["fav_color"] = "blue"

# Get a session value -- this could be called in a different view,

# or many requests later (or both): fav_color = request.session["fav_color"]

# Clear an item from the session: del request.session["fav_color"]

# Check if the session has a given key: if "fav_color" in request.session:

...

其他的映射方法,如 keys() 和 items() 对 request.session 同样有效:下面是一些有效使用 Django sessions 的简单规则:

  • 用正常的字符串作为 key 来访问字典 request.session , 而不是整数、对象或其它什么的。 这不是什么强硬的条规,但值得遵循。

  • Session 字典中以下划线开头的 key 值是 Django 内部保留 key 值。框架只会用很少的几个下划线 开头的 session 变量,除非你知道他们的具体含义,而且愿意跟上 Django的变化,否则,最好 不要用这些下划线开头的变量,它们会让 Django 搅乱你的应用。

  • 不要用一个新对象来替换掉 request.session ,也不要存取其属性,象用普通 Python字典一样用它。

    我们来看个简单的例子。这是个简单到不能再简单的例子:在用户发了一次评论后将

    has_commented 设置为 True ,这是个简单(但不很安全)的、防止用户多次评论的方法。

    def post_comment(request, new_comment):

    if request.session.get('has_commented', False): return HttpResponse("You've already commented.")

    c = comments.Comment(comment=new_comment) c.save() request.session['has_commented'] = True

    return HttpResponse('Thanks for your comment!')下面是一个很简单的站点登录视图(view):

    def login(request): try:

    m = Member.objects.get(username exact=request.POST['username']) if m.password == request.POST['password']:

    request.session['member_id'] = m.id return HttpResponse("You're logged in.")

    except Member.DoesNotExist:

    return HttpResponse("Your username and password didn't match.")这是退出登录,根据 login() :

    def logout(request): try:

    del request.session['member_id'] except KeyError:

    pass

    return HttpResponse("You're logged out.")

    注意

    在实践中,这是很烂的用户登录方式,稍后讨论的认证(authentication )框架会帮你以更健壮和有利的方式来处理这些问题。这些非常简单的例子只是想让你知道这一切是如何工作的。

    设置测试 Cookies

    就像前面提到的,你不能指望所有的浏览器都可以接受 cookie,因此,Django 为了方便,也提供了检查用户浏览器是否接受 cookie 的简单方法。你只需在视图(view)中调用 request.session.set_test_cookie() ,并在后续的视图(view)、而不是当前的视图(view)中检查 request.session.test_cookie_worked() 。

    虽然把 set_test_cookie() 和 test_cookie_worked() 分开的做法看起来有些笨拙,但由于

    cookie 的工作方式,这无可避免。当设置一个 cookie 时候,只能等浏览器下次访问的时候,你才能知道浏览器是否接受 cookie。

    检查 cookie 是否可以正常工作后,你得自己用 delete_test_cookie() 来清除它,这是个好习惯。

    这是个典型例子: def login(request):

    # If we submitted the form... if request.method == 'POST':

    # Check that the test cookie worked (we set it below): if request.session.test_cookie_worked():

    # The test cookie worked, so delete it.

    request.session.delete_test_cookie()

    # In practice, we'd need some logic to check username/password

    # here, but since this is an example... return HttpResponse("You're logged in.")

    # The test cookie failed, so display an error message. If this

    # was a real site we'd want to display a friendlier message. else:

    return HttpResponse("Please enable cookies and try again.")

    # If we didn't post, send the test cookie along with the login form. request.session.set_test_cookie()

    return render_to_response('foo/login_form.html')

    注意

    再次强调,内置的认证函数会帮你帮你做检查的。

    在视图(View)外使用 Session

    从内部来看,每个 session 都只是一个普通的 Django model(在

    django.contrib.sessions.models 中定义)。每个 session 都由一个随机的 32 字节哈希串来标识,并存储于数据库中。由于这是一个普通的 model,你可以用一般的 Django 数据库 API来读取 session。

    >>> from django.contrib.sessions.models import Session

    >>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')

    >>> s.expire_date

    datetime.datetime(2005, 8, 20, 13, 35, 12)

    你得用 get_decoded() 来读取实际的 session 数据,因为 session 字典经过了编码存储。

    >>> s.session_data 'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'

    >>> s.get_decoded()

    {'user_id': 42}

    何时保存 Session

    缺省的情况下,Django 只会在 session 发生变化的时候才会存入数据库,比如说,字典赋值或删除。

    # Session is modified. request.session['foo'] = 'bar'

    # Session is modified. del request.session['foo']

    # Session is modified. request.session['foo'] = {}

    # Gotcha: Session is NOT modified, because this alters

    # request.session['foo'] instead of request.session. request.session['foo']['bar'] = 'baz'

    你可以设置 SESSION_SAVE_EVERY_REQUEST 为 True 来改变这一缺省行为。如果

    SESSION_SAVE_EVERY_REQUEST 设置为 True ,Django 会在每次请求的时候都把 session 存到数据库中,即使没有任何改变。

    注意,会话 cookie 只会在创建和修改的时候才会送出。但如果 SESSION_SAVE_EVERY_REQUEST设置为 True ,会话 cookie 会在每次请求的时候都会送出。同时,每次会话 cookie 送出的时候,其 expires 参数都会更新。

    浏览器关闭即失效会话 vs. 持久会话

    你可能注意到了,Google 给我们发送的cookie 中有 expires=Sun, 17-Jan-2038 19:14:07 GMT;

    cookie 可以有过期时间,这样浏览器就知道什么时候可以删除 cookie 了。如果 cookie 没有设置过期时间,当用户关闭浏览器的时候,cookie 就自动过期了。你可以改变

    SESSION_EXPIRE_AT_BROWSER_CLOSE 的设置来控制 session 框架的这一行为。

    缺省情况下, SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 False ,这样,会话 cookie 可以在用户浏览器中保持有效达 SESSION_COOKIE_AGE 秒(缺省设置是两周,即 1,209,600 秒)。如果你不想用户每次打开浏览器都必须重新登陆的话,用这个参数来帮你。

    如果 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 True ,当浏览器关闭时,Django 会使

    cookie 失效。

    其他的 Session 设置

    除了上面提到的设置,还有一些其他的设置可以影响 Django session 框架如何使用 cookie,详见表 12-2.

    表 12-2. 影响 cookie 行为的设置

    设置

    描述

    缺省值

    表 12-2. 影响 cookie 行为的设置

    设置

    描述

    缺省值

    SESSION_COOKIE_DOMAIN

    session cookie 生效的站点,跨站点生效的 cookie 可以这样设置:

    ``”.lawrence.com”`` None 为标准 cookie

    None

    SESSION_COOKIE_NAME

    用于 session 的 cookie 名称,可以是任何 字符串

    "sessionid"

    SESSION_COOKIE_SECURE

    是否在 session 中使用安全 cookie,如果设置 True , cookie 就会标记为安全, 这意味着 cookie 只会通过 HTTPS 来传输

    False

    技术细节

    如果你还是好奇的话,下面是一些关于 session 框架内部工作方式的技术细节:

    session 字典和普通 Python 对象一样,支持序列化,详见 Python 文档中内置 pickle 模块的部分。

    Session 数据存在数据库表 django_session 中

    Session 数据在需要的时候才会读取,如果你从不使用 request.session , Django 不会动相关数据库表的一根毛。

    Django 只在需要的时候才送出 cookie。如果你压根儿就没有设置任何会话数据,它不会 送出会话 cookie(除非 SESSION_SAVE_EVERY_REQUEST 设置为 True )

    Django session 框架完全而且只能基于 cookie,不会,不会后退到把会话 ID 编码在 URL 中。

    (像某些工具(PHP,JSP)那样)

    这是一个有意而为之的设计,把session 放在URL 中不只是难看,更重要的是这让你的站点 很容易受到攻击——通过 Referer header 进行 session ID”窃听”而实施的攻击。

    如果你还是好奇,阅读源代码是最直接办法,详见 django.contrib.sessions 。

    用户与 Authentication

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

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

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

    2. 验证用户是否拥有执行某种操作的 授权 (通常会通过检查一个权限表来确认)根据这些需求,Django 认证/授权 系统会包含以下的部分:

  • 用户 : 在网站注册的人

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

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

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

  • Profiles : 通过自定义字段扩展用户对象的机制

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

    打开认证支持

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

    1. 根据本章早前的部分确认已经安装了 session 框架,需要确认用户使用 cookie,这样

      sesson 框架才能正常使用。

    2. 将 'django.contrib.auth' 放在你的 INSTALLED_APPS 设置中,然后运行 manage.py syncdb

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

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

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

    # Do something for authenticated users.

    else:

    # Do something for anonymous users.

    使用 User 对象

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

    表 12-3. User 对象属性

    属性

    描述

    username

    必填; 少于等于 30 字符. 只允许字符,数字,下划线

    first_name

    可选; 少于等于 30 字符.

    last_name

    可选; 少于等于 30 字符.

    email

    可选. 邮件地址.

    password

    必填. 密码的摘要 hash(Django 不会存储原始密码),详见密码章节部分

    is_staff

    布尔值. 用户是否拥有网站的管理权限.

    is_active

    布尔值. 是否允许用户登录, 设置为``False``,可以不用删除用户来禁止 用户登录

    is_superuser

    布尔值. 用户是否拥有所有权限,而无需任何显式的权限分配定义

    last_login

    用户最后登录的时间,缺省会设置为当前时间

    date_joined

    创建用户的时间,当用户创建时,缺省的设置为当前的时间

    表 12-4. User 对象方法

    方法

    描述

    is_authenticated()

    如果是真正的 User 对象,返回值恒为 True 。 用于检查用户是否已经通过了认证。通过认证并不意味着 用户拥有任何权限,甚至也不检查该用户是

    否处于激活状 态,这只是表明用户成功的通过了认证。

    is_anonymous()

    如果是个 AnonymousUser ,返回值为 True , 如果是 User 对象,返回值

    为 False 。一般来 说, is_authenticated() 会比这个方法更常用些。

    get_full_name()

    返回值为: first_name 加上 last_name ,以 空格分隔。

    set_password(passwd)

    将用户的密码设置为给定的字符串,实际密码已被哈希 处理。这时并不会

    真正保存 User 对象。

    check_password(passwd)

    如果给定的字符串通过了密码检查,返回 True 。 密码比较已进行了哈希处

    理。

    get_group_permissions()

    返回用户通过所属组获得的权限列表

    get_all_permissions()

    返回用户通过所属组和用户自身权限所获得的所有权限 列表。

    has_perm(perm)

    如果用户拥有给定的权限,返回 True , perm 应形如 "package.codename"

    的格式。如果用户处于 非激活状态,则总是返回 False 。

    has_perms(perm_list)

    如果用户拥有所有给定的权限,返回 True 。 如果用户处于非激活状态,则总是返回 False 。

    has_module_perms(app_label)

    如果用户拥有任何给定 app_label 的权限,返回 True 。如果用户处于非激

    活状态,则总是返回 False

    get_and_delete_messages()

    返回用户的 Message 对象列表,并从队列中删除。

    表 12-4. User 对象方法

    方法

    描述

    email_user(subj, msg)

    给用户发送电子邮件,用 DEFAULT_FROM_EMAIL 的设 置作为发件人。也可以用第 3 个参数 from_email 来 覆盖设置。

    get_profile()

    返回用户的网站自定义 profile,详见 Profile 章节

    最后, User 对象有两个多对多的属性: groups 和 permissions 。 User 对象可以 象使用其他多对多属性的方法一样使用它们。

    # Set a user's groups: myuser.groups = group_list

    # Add a user to some groups: myuser.groups.add(group1, group2,...)

    # Remove a user from some groups: myuser.groups.remove(group1, group2,...)

    # Remove a user from all groups: myuser.groups.clear()

    # Permissions work the same way myuser.permissions = permission_list

    myuser.permissions.add(permission1, permission2, ...) myuser.permissions.remove(permission1, permission2, ...) myuser.permissions.clear()

    登录和退出

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

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

    >>> from django.contrib import auth

    >>> user = auth.authenticate(username='john', password='secret')

    >>> if user is not None:

    ... print "Correct!"

    ... else:

    ... print "Oops, that's wrong!"

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

    下面的例子演示了如何在一个视图中同时使用 authenticate() 和 login() 函数: from django.contrib import auth

    def login(request):

    username = request.POST['username'] password = request.POST['password']

    user = auth.authenticate(username=username, password=password) if user is not None and user.is_active:

    # Correct password, and the user is marked "active" auth.login(request, user)

    # Redirect to a success page.

    return HttpResponseRedirect("/account/loggedin/") else:

    # Show an error page

    return HttpResponseRedirect("/account/invalid/")

    注销一个用户,在你的视图中使用 django.contrib.auth.logout() 。该函数接受一个

    HttpRequest 对象作为参数,没有返回值。 from django.contrib import auth

    def logout(request): auth.logout(request)

    # Redirect to a success page.

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

    在实际中,你一般不需要自己写登录/登出的函数;认证系统提供了一系例视图用来处理登录和登出。

    使用认证视图的第一步是把它们写在你的 URLconf 中。 你需要这样写: from django.contrib.auth.views import login, logout

    urlpatterns = patterns('',

    # existing patterns here... (r'^accounts/login/$', login), (r'^accounts/logout/$', logout),

    )

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

    缺省情况下, login 视图渲染 registragiton/login.html 模板(可以通过视图的额外参数

    template_name 修改这个模板名称)。这个表单必须包含 username 和 password 域。如下示例:

    {% extends "base.html" %}

    {% block content %}

    {% if form.errors %}

    <p class="error">Sorry, that's not a valid username or password</p>

    {% endif %}

    <form action='.' method='post'>

    <label for="username">User name:</label>

    <input type="text" name="username" value="" id="username">

    <label for="password">Password:</label>

    <input type="password" name="password" value="" id="password">

    <input type="submit" value="login" />

    <input type="hidden" name="next" value="{{ next|escape }}" />

    <form action='.' method='post'>

    {% endblock %}

    如果用户登录成功,缺省会重定向到 /accounts/profile 。表单中有一个 hidden 域叫 next ,可以用在登录后指定 url。也可以把这个值(指定的 url)作为 GET 参数传递给 login 视图, 并且会作为 next 变量添加到 context 中。

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

    限制已登录用户的访问

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

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

    from django.http import HttpResponseRedirect def my_view(request):

    if not request.user.is_authenticated():

    return HttpResponseRedirect('/login/?next=%s' % request.path)

    # ...

    或者显示一个出错信息: def my_view(request):

    if not request.user.is_authenticated():

    return render_to_response('myapp/login_error.html')

    # ...

    作为一个快捷方式, 你可以使用便捷的 login_required 修饰符: from django.contrib.auth.decorators import login_required

    @login_required

    def my_view(request):

    # ...

    login_required 做下面的事情:

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

  • 如果用户已经登录, 正常地执行视图函数. 视图代码就可以假定用户已经登录了.对通过测试的用户限制访问

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

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

    def vote(request):

    if request.user.is_authenticated() and request.user.has_perm('polls.can_vote')):

    # vote here else:

    return HttpResponse("You can't vote in this poll.")

    并且 Django 有一个称为 user_passes_test 的简洁方式。它根据情况使用参数并且产生特殊装饰符。

    def user_can_vote(user):

    return user.is_authenticated() and user.has_perm("polls.can_vote")

    @user_passes_text(user_can_vote, login_url="/login/") def vote(request):

    # Code here can assume a logged-in user with the correct permission.

    ...

    user_passes_test 使用一个必需的参数:一个可调用的方法,它存在 User 对象并当此用户允许查看该页面时返回 True 。 注意 user_passes_test 不会自动检查 User 是否认证,你应该自己做这件事。

    例子中我们也展示了第二个可选的参数 login_url ,它让你指定你的登录页面的 URL(默认为 /accounts/login/ )。

    既然检查用户是否有一个特殊权限是相对常见的任务,Django 为这种情形提供了一个捷径:

    permission_required() 装饰器 使用这个装饰器,前面的例子可以这样写: from django.contrib.auth.decorators import permission_required

    @permission_required('polls.can_vote', login_url="/login/") def vote(request):

    # ...

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

    '/accounts/login/' 。限制通用视图的访问

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

    from dango.contrib.auth.decorators import login_required from django.views.generic.date_based import object_detail

    @login_required

    def limited_object_detail(*args, **kwargs): return object_detail(*args, **kwargs)

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

    管理 Users, Permissions 和 Groups

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

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

    创建用户

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

    >>> from django.contrib.auth.models import User

    >>> user = User.objects.create_user(username='john',

    ... email='jlennon@beatles.com',

    ... password='glass onion')

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

    >>> user.is_staff = True

    >>> user.save()修改密码

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

    >>> user = User.objects.get(username='john')

    >>> user.set_password('goo goo goo joob')

    >>> user.save()

    除非你清楚的知道自己在做什么,否则不要直接修改 password 属性。其中保存的是密码的

    加入 salt 的 hash 值 ,所以不能直接编辑。

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

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

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

    sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4

    User.set_password() 和 User.check_password() 函数在后台处理和检查这些值。一个加入 salt 的哈希算法是某种毒品吗?

    不是,一个 加入 salt 值的哈希算法 与毒品完全无关;事实上它提供了一种通用的方法来保证密码存储的安全。一次 哈希 是一次单向的密写过程,你能容易地计算出一个给定值的哈希码,但是几乎不可能从一个哈希码解出它的原值。

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

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

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

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

    加入 salt 值得 hash 并不是绝对安全的存储密码的方法,然而在安全和方便之间有很大的中间地带需要我们来做决定。

    处理注册

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

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

    from django import oldforms as forms

    from django.http import HttpResponseRedirect from django.shortcuts import render_to_response

    from django.contrib.auth.forms import UserCreationForm

    def register(request):

    form = UserCreationForm()

    if request.method == 'POST': data = request.POST.copy()

    errors = form.get_validation_errors(data) if not errors:

    new_user = form.save(data)

    return HttpResponseRedirect("/books/")

    else:

    data, errors = {}, {}

    return render_to_response("registration/register.html", { 'form' : forms.FormWrapper(form, data, errors)

    })

    这个表单构想了一个叫 registration/register.html 的模板. 这里是一个这个模板的可能的样子的例子:

    {% extends "base.html" %}

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

    {% block content %}

    <h1>Create an account</h1>

    <form action="." method="post">

    {% if form.error_dict %}

    <p class="error">Please correct the errors below.</p>

    {% endif %}

    {% if form.username.errors %}

    {{ form.username.html_error_list }}

    {% endif %}

    <label for="id_username">Username:</label> {{ form.username }}

    {% if form.password1.errors %}

    {{ form.password1.html_error_list }}

    {% endif %}

    <label for="id_password1">Password: {{ form.password1 }}

    {% if form.password2.errors %}

    {{ form.password2.html_error_list }}

    {% endif %}

    <label for="id_password2">Password (again): {{ form.password2 }}

    <input type="submit" value="Create the account" />

    </label>

    {% endblock %}备注

    在本书出版之时, django.contrib.auth.forms.UserCreationForm 是一个 oldforms 表单.参看 http://www.djangoproject.com/documentation/0.96/forms/ 可以获取有关

    oldforms 的详细信息. 转换到有关 newforms 的内容在第 7 章中将会讲述, newforms 功能将会在不远的将来完成.

    在模板中使用认证数据

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

    备注

    从技术上来说,只有当你使用了 RequestContext 并且 TEMPLATE_CONTEXT_PROCESSORS 设置包含了 "django.core.context_processors.auth" (默认情况就是如此)时,这些变量才能在模板 context 中使用。更详细的内容,也请参考第 10 章。

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

    {% if user.is_authenticated %}

    <p>Welcome, {{ user.username }}. Thanks for logging in.</p>

    {% else %}

    <p>Welcome, new user. Please log in.</p>

    {% endif %}

    这些用户的权限信息存储在 {{ perms }} 模板变量中。这是一个在模板中使用很方便的代理,其中包含一些权限相关函数的简写。

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

    {{ perms.polls.can_vote }} 这样的形式,来检查一个用户是否拥有特定的权限。这样你就可以在模板中的 {% if %} 语句中检查权限:

    {% if perms.polls %}

    <p>You have permission to do something in the polls app.</p>

    {% if perms.polls.can_vote %}

    <p>You can vote!</p>

    {% endif %}

    {% else %}

    <p>You don't have permission to do anything in the polls app.</p>

    {% endif %}

    其他一些功能:权限,组,消息和档案

    在认证框架中还有其他的一些功能。我们会在接下来的几个部分中进一步地了解它们。

    权限

    权限可以很方便地标识用户和用户组可以执行的操作。它们被 Django 的 admin 管理站点所使用,你也可以在你自己的代码中使用它们。

    Django 的 admin 站点如下使用权限:

  • 只有设置了 add 权限的用户才能使用添加表单,添加对象的视图。

  • 只有设置了 change 权限的用户才能使用变更列表,变更表格,变更对象的视图。

  • 只有设置了 delete 权限的用户才能删除一个对象。

    权限是根据每一个类型的对象而设置的,并不具体到对象的特定实例。例如,我们可以允许

    Mary 改变新故事,但是目前还不允许设置 Mary 只能改变自己创建的新故事,或者根据给定的状态,出版日期或者 ID 号来选择权限。

    这三个基本权限:添加,变更和删除,会被自动添加到所有的 Django 模型中,只要改模型包含 class Admin 。当你执行 manage.py syncdb 的时候,这些就被自动添加到

    auth_permission 数据表中。

    权限以 "<app>.<action>_<object_name>" 的形式出现。如果你有一个 polls 的应用,包含一个 Choice 模型,你就有以下三个权限,分别叫做 "polls.add_choice" , "polls.change_choice" ,和 "polls.delete_choice" 。

    注意,如果当你运行 syncdb 时,模型中没有包含 class Admin ,该模型对应的权限就不会被创建。如果你在初始化数据库以后,又在自己的模型中加入了 class Admin ,你就需要重新运行 syncdb 来为应用加入权限。

    你也可以通过设置 Meta 中的 permissions 属性,来为给定的模型定制权限。下面的例子创建了三个自定义的权限:

    class USCitizen(models.Model):

    # ...

    class Meta:

    permissions = (

    # Permission identifier human-readable permission name ("can_drive", "Can drive"),

    ("can_vote", "Can vote in elections"),

    ("can_drink", "Can drink alcohol"),

    )

    当你运行 syncdb 时,额外的权限才会被加入;你需要自己在视图中添加权限相关的代码。

    就跟用户一样,权限也就是 Django 模型中的 django.contrib.auth.models 。因此如果你愿意,你也可以通过 Django 的数据库 API 直接操作权限。

    组提供了一种通用的方式来让你按照一定的权限规则和其他标签将用户分类。一个用户可以隶属于任何数量的组。

    在一个组中的用户自动获得了赋予该组的权限。例如, Site editors 组拥有

    can_edit_home_page 权限,任何在该组中的用户都拥有这个权限。

    组也可以通过给定一些用户特殊的标记,来扩展功能。例如,你创建了一个 'Special users'组,并且允许组中的用户访问站点的一些 VIP 部分,或者发送 VIP 的邮件消息。

    和用户管理一样,admin 接口是管理组的最简单的方法。然而,组也就是 Django 模型 django.contrib.auth.models ,因此你可以使用 Django 的数据库 API,在底层访问这些组。

    消息

    消息系统会为给定的用户接收消息。每个消息都和一个 User 相关联。其中没有超时或者时间戳的概念。

    在每个成功的操作以后,Django 的 admin 管理接口就会使用消息机制。例如,当你创建了一个对象,你会在 admin 页面的顶上看到 The object was created successfully 的消息。

    你也可以使用相同的 API 在你自己的应用中排队接收和现实消息。API 非常地简单:

  • 要创建一条新的消息,使用 user.message_set.create(message='message_text') 。

  • 要获得/删除消息,使用 user.get_and_delete_messages() ,这会返回一个 Message对象的列表,并且从队列中删除返回的项。

  • 在例子视图中,系统在创建了播放单(playlist)以后,为用户保存了一条消息。 def create_playlist(request, songs):

    # Create the playlist with the given songs.

    # ...

    request.user.message_set.create(

    message="Your playlist was added successfully."

    )

    return render_to_response("playlists/create.html", context_instance=RequestContext(request))

    当使用 RequestContext ,当前登录的用户以及他(她)的消息,就会以模板变量

    {{ messages }} 出现在模板的 context 中。下面是显示消息的一个例子模板代码:

    {% if messages %}

    <ul>

    {% for message in messages %}

    <li>{{ message }}</li>

    {% endfor %}

    </ul>

    {% endif %}

    需要注意的是 RequestContext 会在后台调用 get_and_delete_messages ,因此即使你没有显示它们,它们也会被删除掉。

    最后注意,这个消息框架只能服务于在用户数据库中存在的用户。如果要向匿名用户发送消息,请直接使用会话框架。

    档案

    最后一个难题是档案系统.为了理解什么是档案,让我们先看看问题.

    简单来说,许多网站需要存储比标准 User 对象更多的用户信息。为了解决这个问题,大多数网站都会有不同的额外字段。所以,Django 提供一个轻量级的方式定义档案对象链接到指定的用户。这个档案对象在每个项目中可以是不同的,甚至可以为同一数据库服务的不同的站点处理不同的档案。

    创建档案的第一步是定义一个模型(model)来存储档案信息。Django 对这个模型所做的唯一的限制是,必须要包含唯一的一个对 User 模型的 ForeignKey ,而且这个字段必须要叫做 user 。其他的字段可以由你自己掌控。下面是一个档案模型的例子:

    from django.db import models

    from django.contrib.auth.models import User

    class MySiteProfile(models.Model):

    # This is the only required field

    user = models.ForeignKey(User, unique=True)

    # The rest is completely up to you...

    favorite_band = models.CharField(maxlength=100, blank=True) favorite_cheese = models.CharField(maxlength=100, blank=True) lucky_number = models.IntegerField()

    下一步,你需要告诉 Django 去哪里查找档案对象。你可以通过设置模型中的

    AUTH_PROFILE_MODULE 变量达到这个目的。因此,如果你的模型包含在 myapp 这个应用中,你就需要如下编写你的设置文件:

    AUTH_PROFILE_MODULE = "myapp.mysiteprofile"

    一旦完成,你就可以通过调用 user.get_profile() 函数来获得用户档案。如果

    AUTH_PROFILE_MODULE 变量没有设置,这个函数可能会抛出 SiteProfileNotAvailable 异常;如果这个用户不存在档案,也可能会抛出 DoesNotExist 异常(通常情况下,你会捕获这个 异常并在当时创建一个新的档案)。

    接下来?

    是的,会话和认证系统有太多的东西要学。大多数情况下,你并不需要本章所提到的所有功能。然而当你需要允许用户之间复杂的互操作时,所有的功能都能使用就显得很重要了。

    在下一章节中,我们会来深入了解 Django 建立在会话/用户系统之上的一个系统:评论应用。它允许你很方便地以匿名用户或者注册用户的身份,向任意类型的对象添加评论。让我们继续向前吧。