正在添加身份验证¶
Pyramid 为 authentication 和 authorization . 在本节中,我们将只关注认证API,以向wiki添加登录和注销功能。
我们将通过以下步骤实现身份验证:
Add a security policy (
security.py
).添加路由
/login
和/logout
(routes.py
)添加登录和注销视图 (
views/auth.py
)添加登录模板 (
login.jinja2
)根据用户的身份验证状态向每个页面添加“登录”和“注销”链接 (
layout.jinja2
)使现有视图验证用户状态 (
views/default.py
)重定向到
/login
当用户未登录并且被拒绝访问任何需要权限的视图时 (views/auth.py
)如果拒绝登录用户访问任何需要权限的视图,则显示自定义“403禁止”页面 (
views/auth.py
)
正在验证请求¶
核心 Pyramid 身份验证是 security policy 用于识别来自 request
以及处理跨请求跟踪用户所需的低级登录和注销操作(通过cookie、头文件或您可以想象的任何其他方式)。
添加安全策略¶
更新 tutorial/security.py
with the following content:
1from pyramid.authentication import AuthTktCookieHelper
2from pyramid.csrf import CookieCSRFStoragePolicy
3from pyramid.request import RequestLocalCache
4
5from . import models
6
7
8class MySecurityPolicy:
9 def __init__(self, secret):
10 self.authtkt = AuthTktCookieHelper(secret)
11 self.identity_cache = RequestLocalCache(self.load_identity)
12
13 def load_identity(self, request):
14 identity = self.authtkt.identify(request)
15 if identity is None:
16 return None
17
18 userid = identity['userid']
19 user = request.dbsession.query(models.User).get(userid)
20 return user
21
22 def identity(self, request):
23 return self.identity_cache.get_or_create(request)
24
25 def authenticated_userid(self, request):
26 user = self.identity(request)
27 if user is not None:
28 return user.id
29
30 def remember(self, request, userid, **kw):
31 return self.authtkt.remember(request, userid, **kw)
32
33 def forget(self, request, **kw):
34 return self.authtkt.forget(request, **kw)
35
36def includeme(config):
37 settings = config.get_settings()
38
39 config.set_csrf_storage_policy(CookieCSRFStoragePolicy())
40 config.set_default_csrf_options(require_csrf=True)
41
42 config.set_security_policy(MySecurityPolicy(settings['auth.secret']))
在这里,我们定义了一个新的安全策略,名为 MySecurityPolicy
,它实现了大部分 pyramid.interfaces.ISecurityPolicy
接口,通过跟踪 identity 使用由实现的签名Cookie pyramid.authentication.AuthTktCookieHelper
(第8-34行)。安全策略输出经过身份验证的 tutorial.models.User
对象作为登录用户的 identity ,可通过以下方式获得 request.identity
。
我们的新 security policy 定义应用程序如何记住、忘记和识别用户。它还处理授权,我们将在下一章讨论(如果您想知道为什么我们没有实现 permits
方法)。
识别当前用户只需几个步骤:
Pyramid 调用策略上的方法,请求标识、用户标识或执行操作的权限。
政策从打电话开始
pyramid.request.RequestLocalCache.get_or_create()
加载标识。这个
MySecurityPolicy.load_identity
方法要求cookie助手从请求中提取标识。这个值是None
如果cookie丢失或内容无法验证。然后策略将身份转换为
tutorial.models.User
对象,方法是在数据库中查找记录。这是一个很好的地方来确认用户实际上被允许访问我们的应用程序。例如,可能他们被标记为删除或禁止,我们应该返回None
而不是user
对象。结果存储在
identity_cache
它确保后续调用为请求返回相同的标识对象。
最后, pyramid.request.Request.identity
包含以下任一项 None
或者是 tutorial.models.User
实例。
注意 identity_cache
是可选的,但在大多数情况下它有几个优点:
它提高了性能,因为在请求的生命周期内,身份对于许多操作都是必需的。
它提供跨方法调用的一致性,以确保在处理请求时标识不会更改。
由各个安全策略和应用程序来确定缓存的最佳方法。具有长时间运行请求的应用程序可能希望避免缓存标识,或者跟踪一些额外的元数据,以便定期根据身份验证源重新验证它。
添加新设置¶
我们的身份验证策略需要一个新设置, auth.secret
. 打开文件 development.ini
并在下面添加突出显示的行:
19retry.attempts = 3
20
21auth.secret = seekrit
最后,最佳实践告诉我们在每个环境中使用不同的秘密,所以要开放 production.ini
并添加另一个秘密:
17retry.attempts = 3
18
19auth.secret = real-seekrit
和 testing.ini
:
17retry.attempts = 3
18
19auth.secret = test-seekrit
添加权限检查¶
Pyramid 完全支持声明性授权,我们将在下一章介绍。然而,许多人希望自己的脚湿了,只是对一些基本形式的本土授权的认证感兴趣。下面我们将展示如何实现wiki的简单安全目标,现在我们可以跟踪用户的登录状态。
记住我们的目标:
只允许
editor
和basic
登录用户以创建新页面。只允许
editor
用户和页面创建者(可能是basic
用户)编辑页面。
打开文件 tutorial/views/default.py
并修复以下导入:
3from pyramid.httpexceptions import (
4 HTTPForbidden,
5 HTTPNotFound,
6 HTTPSeeOther,
7)
插入高亮显示的线。
在同一个文件中,现在编辑 edit_page
查看功能:
44@view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2')
45def edit_page(request):
46 pagename = request.matchdict['pagename']
47 page = request.dbsession.query(models.Page).filter_by(name=pagename).one()
48 user = request.identity
49 if user is None or (user.role != 'editor' and page.creator != user):
50 raise HTTPForbidden
51 if request.method == 'POST':
52 page.data = request.params['body']
53 next_url = request.route_url('view_page', pagename=page.name)
54 return HTTPSeeOther(location=next_url)
55 return dict(
56 pagename=page.name,
57 pagedata=page.data,
58 save_url=request.route_url('edit_page', pagename=page.name),
59 )
只需更改突出显示的行。
如果用户未登录或用户不是页面的创建者 and 不是一个 editor
然后我们提高 HTTPForbidden
.
在同一个文件中,现在编辑 add_page
查看功能:
61@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2')
62def add_page(request):
63 user = request.identity
64 if user is None or user.role not in ('editor', 'basic'):
65 raise HTTPForbidden
66 pagename = request.matchdict['pagename']
67 if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
68 next_url = request.route_url('edit_page', pagename=pagename)
69 return HTTPSeeOther(location=next_url)
70 if request.method == 'POST':
71 body = request.params['body']
72 page = models.Page(name=pagename, data=body)
73 page.creator = request.identity
74 request.dbsession.add(page)
75 next_url = request.route_url('view_page', pagename=pagename)
76 return HTTPSeeOther(location=next_url)
77 save_url = request.route_url('add_page', pagename=pagename)
78 return dict(pagename=pagename, pagedata='', save_url=save_url)
只需更改突出显示的行。
如果用户未登录或不在 basic
或 editor
角色,然后我们提出 HTTPForbidden
,这将触发我们的禁用视图来计算响应。但是,我们稍后将挂接此链接以重定向到登录页面。另外,现在我们有了 request.identity
,我们不再需要将创建者硬编码为 editor
用户,所以我们终于可以放弃那次黑客攻击了。
这些简单的检查应该保护我们的观点。
登录退出¶
现在我们已经能够检测登录用户,我们需要添加 /login
和 /logout
视图,以便他们实际登录和注销!
添加路由 /login
和 /logout
¶
回到 tutorial/routes.py
并添加这两条突出显示的路线:
3 config.add_route('view_wiki', '/')
4 config.add_route('login', '/login')
5 config.add_route('logout', '/logout')
6 config.add_route('view_page', '/{pagename}')
备注
必须添加前面的行 之前 以下 view_page
路由定义:
6 config.add_route('view_page', '/{pagename}')
这是因为 view_page
的路由定义使用catch all“替换标记” /{{pagename}}
(见 路由模式语法 ,它将捕获之前注册的任何路由尚未捕获的任何路由。因此,为了 login
和 logout
视图要有机会被匹配(或“捕获”),它们必须高于 /{{pagename}}
.
添加登录、注销和禁止的视图¶
创建新文件 tutorial/views/auth.py
,并添加以下代码:
1from pyramid.csrf import new_csrf_token
2from pyramid.httpexceptions import HTTPSeeOther
3from pyramid.security import (
4 remember,
5 forget,
6)
7from pyramid.view import (
8 forbidden_view_config,
9 view_config,
10)
11
12from .. import models
13
14
15@view_config(route_name='login', renderer='tutorial:templates/login.jinja2')
16def login(request):
17 next_url = request.params.get('next', request.referrer)
18 if not next_url:
19 next_url = request.route_url('view_wiki')
20 message = ''
21 login = ''
22 if request.method == 'POST':
23 login = request.params['login']
24 password = request.params['password']
25 user = (
26 request.dbsession.query(models.User)
27 .filter_by(name=login)
28 .first()
29 )
30 if user is not None and user.check_password(password):
31 new_csrf_token(request)
32 headers = remember(request, user.id)
33 return HTTPSeeOther(location=next_url, headers=headers)
34 message = 'Failed login'
35 request.response.status = 400
36
37 return dict(
38 message=message,
39 url=request.route_url('login'),
40 next_url=next_url,
41 login=login,
42 )
43
44@view_config(route_name='logout')
45def logout(request):
46 next_url = request.route_url('view_wiki')
47 if request.method == 'POST':
48 new_csrf_token(request)
49 headers = forget(request)
50 return HTTPSeeOther(location=next_url, headers=headers)
51
52 return HTTPSeeOther(location=next_url)
53
54@forbidden_view_config(renderer='tutorial:templates/403.jinja2')
55def forbidden_view(exc, request):
56 if not request.is_authenticated:
57 next_url = request.route_url('login', _query={'next': request.url})
58 return HTTPSeeOther(location=next_url)
59
60 request.response.status = 403
61 return {}
此代码向应用程序添加三个新视图:
这个
login
视图呈现登录表单并处理来自登录表单的日志,根据我们的users
数据库中的表。检查是通过首先找到
User
在数据库中记录,然后使用user.check_password
方法比较哈希密码。在权限边界处,我们确定使用
pyramid.csrf.new_csrf_token()
. 如果我们使用的是会话,我们也会希望它失效。如果凭据有效,则我们使用身份验证策略将用户的ID存储在响应中,使用
pyramid.security.remember()
.最后,用户被重定向回他们试图访问的页面。 (
next
)或者首页作为回退。此参数由我们的禁止视图使用,如下所述,以完成登录工作流。这个
logout
视图处理对的请求/logout
通过使用清除凭据pyramid.security.forget()
,然后将它们重定向到首页。在权限边界处,我们确定使用
pyramid.csrf.new_csrf_token()
. 如果我们使用的是会话,我们也会希望它失效。这个
forbidden_view
注册时使用pyramid.view.forbidden_view_config
装饰者。这是个特色菜 exception view ,当pyramid.httpexceptions.HTTPForbidden
引发异常。默认情况下,视图将返回“403禁止”响应并显示
403.jinja2
模板(添加如下)。但是,如果用户没有登录,此视图将通过将用户重定向到
/login
. 为了方便起见,它还设置了next=
当前URL(禁止访问的URL)的查询字符串。这样,如果用户成功登录,他们将被发送回他们试图访问的页面。
添加 login.jinja2
模板¶
创造 tutorial/templates/login.jinja2
with the following content:
{% extends 'layout.jinja2' %}
{% block title %}Login - {% endblock title %}
{% block content %}
<p>
<strong>
Login
</strong><br>
{{ message }}
</p>
<form action="{{ url }}" method="post">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="next" value="{{ next_url }}">
<div class="form-group">
<label for="login">Username</label>
<input type="text" name="login" value="{{ login }}">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password">
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log In</button>
</div>
</form>
{% endblock content %}
上面的模板在我们刚刚添加的登录视图中被引用 tutorial/views/auth.py
.
添加“登录”和“注销”链接¶
正常开放 tutorial/templates/layout.jinja2
并添加以下代码,如突出显示的行所示。
35 <div class="content">
36 {% if not request.is_authenticated %}
37 <p class="pull-right">
38 <a href="{{ request.route_url('login') }}">Login</a>
39 </p>
40 {% else %}
41 <form class="pull-right" action="{{ request.route_url('logout') }}" method="post">
42 {{request.identity.name}}
43 <input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
44 <button class="btn btn-link" type="submit">Logout</button>
45 </form>
46 {% endif %}
47 {% block content %}{% endblock %}
48 </div>
这个 request.identity
将会是 None
如果用户未经过身份验证,或者 tutorial.models.User
如果用户已通过身份验证,则返回。此检查将使注销链接仅在用户登录时显示,反之,登录链接仅在用户注销时显示。
添加 403.jinja2
模板¶
创造 tutorial/templates/403.jinja2
with the following content:
{% extends "layout.jinja2" %}
{% block content %}
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
<p class="lead"><span class="font-semi-bold">403</span> Forbidden</p>
{% endblock content %}
上面的模板在我们刚刚添加的禁止视图中被引用 tutorial/views/auth.py
.
在浏览器中查看应用程序¶
我们最终可以在浏览器中检查我们的应用程序(请参见 启动应用程序 )启动浏览器并访问以下每个URL,检查结果是否符合预期:
http://localhost:6543/ invokes the
view_wiki
查看。这总是重定向到view_page
视图FrontPage
页面对象。它可由任何用户执行。http://localhost:6543/login invokes the
login
视图,将显示一个登录表单。在每个页面上,当用户未通过身份验证时,在右上角有一个“Login”链接,否则,当用户通过身份验证时,它是一个“Logout”链接。为凭据提供用户名
editor
和密码editor
或用户名basic
和密码basic
将对用户进行身份验证并授予该组访问权限。登录后(点击编辑或添加页面并提交有效凭据),我们将在右上角看到“注销”链接。当我们点击它时,我们被注销,重定向回首页,“登录”链接显示在右上角。
http://localhost:6543/FrontPage invokes the
view_page
视图FrontPage
页面对象。http://localhost:6543/FrontPage/edit_page invokes the
edit_page
的视图FrontPage
页面对象。它只能由editor
用户。如果不同的用户调用它,则会显示“403禁止”页面。如果匿名用户调用它,则会显示一个登录表单。http://localhost:6543/add_page/SomePageName invokes the
add_page
页面的视图。如果该页已经存在,则它会将用户重定向到edit_page
页面对象的视图。它可以由editor
或basic
用户。如果匿名用户调用它,则会显示一个登录表单。http://localhost:6543/SomePageName/edit_page invokes the
edit_page
视图,或者在该页不存在时生成错误。它可以由basic
用户(如果页面是由该用户在上一步中创建的)。如果该页是由editor
用户,则应显示以下内容的登录页basic
用户。