正在添加身份验证

Pyramidauthenticationauthorization . 在本节中,我们将只关注认证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 方法)。

识别当前用户只需几个步骤:

  1. Pyramid 调用策略上的方法,请求标识、用户标识或执行操作的权限。

  2. 政策从打电话开始 pyramid.request.RequestLocalCache.get_or_create() 加载标识。

  3. 这个 MySecurityPolicy.load_identity 方法要求cookie助手从请求中提取标识。这个值是 None 如果cookie丢失或内容无法验证。

  4. 然后策略将身份转换为 tutorial.models.User 对象,方法是在数据库中查找记录。这是一个很好的地方来确认用户实际上被允许访问我们的应用程序。例如,可能他们被标记为删除或禁止,我们应该返回 None 而不是 user 对象。

  5. 结果存储在 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的简单安全目标,现在我们可以跟踪用户的登录状态。

记住我们的目标:

  • 只允许 editorbasic 登录用户以创建新页面。

  • 只允许 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)

只需更改突出显示的行。

如果用户未登录或不在 basiceditor 角色,然后我们提出 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}} (见 路由模式语法 ,它将捕获之前注册的任何路由尚未捕获的任何路由。因此,为了 loginlogout 视图要有机会被匹配(或“捕获”),它们必须高于 /{{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 .

添加 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 页面对象的视图。它可以由 editorbasic 用户。如果匿名用户调用它,则会显示一个登录表单。

  • http://localhost:6543/SomePageName/edit_page invokes the edit_page 视图,或者在该页不存在时生成错误。它可以由 basic 用户(如果页面是由该用户在上一步中创建的)。如果该页是由 editor 用户,则应显示以下内容的登录页 basic 用户。