是时候承认了:我们有意的避开了web开发中极其重要的方面。到目前为止,我们都在假定,网站流量是大量的匿名用户带来的。
这当然不对,浏览器的背后都是活生生的人(至少某些时候是)。我们忽略了一件重要的事情:互联网服务于人而不是机器。要开发一个真正令人心动的网站,我们必须面对浏览器后面活生生的人。
很不幸,这并不容易。HTTP被设计为”无状态”,每次请求都处于相同的空间中。在一次请求和下一次请求之间没有任何状态保持,我们无法根据请求的任何方面(IP地址,用户代理等)来识别来自同一人的连续请求。
在本章中你将学会如何搞定状态的问题。好了,我们会从较低的层次(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,可以用它在页面上显示你的用户名。
在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。
参数 | 缺省值 | 描述 |
---|---|---|
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对于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 ,以标识用户已经登录。犯这类错误的站点数量多的令人难以置信; 绕过这些网站的安全系统也是易如反掌。
由于存在的限制与安全漏洞,cookies和持续性会话已经成为Web开发中令人头疼的典范。好消息是,Django的目标正是高效的“头疼杀手”,它自带的session框架会帮你搞定这些问题。
你可以用session 框架来存取每个访问者任意数据,这些数据在服务器端存储,并用通过cookie来传输数据摘要。cookies只存储数据的哈希会话ID,而不是数据本身,从而避免了大部分的常见cookie问题。
下面我们来看看如何打开session功能,并在视图中使用它。
Sessions 功能是通过一个中间件(middleware)和一个模型(model)来实现的。要打开sessions功能,需要以下几步操作:
编辑 MIDDLEWARE_CLASSES 配置,确保 MIDDLEWARE_CLASSES 中包含 'django.contrib.sessions.middleware.SessionMiddleware'
确认 INSTALLED_APPS 中有 'django.contrib.sessions' (如果你是刚打开这个应用,别忘了运行 manage.py syncdb )
如果项目是用 startproject 来创建的,配置文件中都已经安装了这些东西,除非你自己删除,正常情况下,你无需任何设置就可以使用session功能。
如果不需要session功能,你可以删除 MIDDLEWARE_CLASSES 设置中的 SessionMiddleware 和 INSTALLED_APPS 设置中的 'django.contrib.sessions' 。虽然这只会节省很少的开销,但积少成多啊。
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 )框架会帮你以更健壮和有利的方式来处理这些问题。这些非常简单的例子只是想让你知道这一切是如何工作的。
就像前面提到的,你不能指望所有的浏览器都可以接受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')
注意
再次强调,内置的认证函数会帮你做检查的。
从内部来看,每个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}
缺省的情况下,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 参数都会更新。
你可能注意到了,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失效。
除了上面提到的设置,还有一些其他的设置可以影响Django session框架如何使用cookie,详见表 12-2.
设置 | 描述 | 缺省值 |
---|---|---|
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 。
现在,我们通过浏览器连接真实用户的目标已经完成一半了。通过session,我们可以在多次浏览器请求中保持数据, 接下来的部分就是用session来处理用户登录了。当然,不能仅凭用户的一面之词,我们就相信,所以我们需要认证。
当然了,Django 也提供了工具来处理这样的常见任务(就像其他常见任务一样)。Django 用户认证系统处理用户帐号,组,权限以及基于cookie的用户会话。这个系统一般被称为 auth/auth (认证与授权)系统,这个系统的名称同时也表明了用户常见的两步处理。我们需要
验证 (认证) 用户是否是他所宣称的用户(一般通过查询数据库验证其用户名和密码)
验证用户是否拥有执行某种操作的 授权 (通常会通过检查一个权限表来确认)
根据这些需求,Django 认证/授权 系统会包含以下的部分:
用户 : 在网站注册的人
权限 : 用于标识用户是否拥有某种操作的二进制(yes/no)标志
组 :一种可以将标记和权限应用于多个用户的常用方法
Messages : 向用户显示队列式的系统消息的常用方法
Profiles : 通过自定义字段扩展用户对象的机制
如果你已经用了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'
这样安装后,我们就可以在视图(view)的函数中用处理user了。在视图中存取users,主要用 request.user ;这个对象表示当前已登录的用户,如果用户还没登录,这就是一个 匿名 对象(细节见下)
你可以很容易的通过 is_authenticated() 方法来判断一个用户是否已经登录了
if request.user.is_authenticated(): # Do something for authenticated users. else: # Do something for anonymous users.
User 实例一般从 request.user ,或是其他下面即将要讨论到的方法取得,它有很多属性和方法。 AnonymousUser 对象模拟了 部分 的接口,但不是全部,在把它当成真正的user对象 使用前,你得检查一下 user.is_authenticated()
属性 | 描述 |
---|---|
username | 必填; 少于等于30字符. 只允许字符,数字,下划线 |
first_name | 可选; 少于等于30字符. |
last_name | 可选; 少于等于30字符. |
可选. 邮件地址. | |
password | 必填. 密码的摘要hash(Django不会存储原始密码),详见密码章节部分 |
is_staff | 布尔值. 用户是否拥有网站的管理权限. |
is_active | 布尔值. 是否允许用户登录, 设置为``False``,可以不用删除用户来禁止 用户登录 |
is_superuser | 布尔值. 用户是否拥有所有权限,而无需任何显式的权限分配定义 |
last_login | 用户最后登录的时间,缺省会设置为当前时间 |
date_joined | 创建用户的时间,当用户创建时,缺省的设置为当前的时间 |
方法 | 描述 |
---|---|
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 对象列表,并从队列中删除。 |
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视图,这个参数会成为Context中名为 next 的变量,你可以把这个变量设置给表单中对应的隐含字段。
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 。
管理认证系统最简单的方法是通过管理界面。 第六章讨论了怎样使用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建立在会话/用户系统之上的一个系统:评论应用。它允许你很方便地以匿名用户或者注册用户的身份,向任意类型的对象添加评论。让我们继续向前吧。
关于本评注系统
本站使用上下文关联的评注系统来收集反馈信息。不同于一般对整章做评注的做法, 我们允许你对每一个独立的“文本块”做评注。一个“文本块”看起来是这样的:
一个“文本块”是一个段落,一个列表项,一段代码,或者其他一小段内容。 你选中它会高亮度显示:
要对文本块做评注,你只需要点击它旁边的标识块:
我们会仔细阅读每个评论,如果可能的话我们也会把评注考虑到未来的版本中去:
如果你愿意你的评注被采用,请确保留下你的全名 (注意不是昵称或简称)
Many, many thanks to Jack Slocum; the inspiration and much of the code for the comment system comes from Jack's blog, and this site couldn't have been built without his wonderful
YAHOO.ext
library. Thanks also to Yahoo for YUI itself.