添加授权和身份验证

Pyramid 为 authentication 和 authorization . 我们将利用这两个特性为我们的应用程序提供安全性。我们的应用程序目前允许任何有权访问服务器的人查看、编辑和向我们的wiki添加页面。我们将改变它,只允许那些 组 已命名 group:editors 添加和编辑wiki页面。我们将继续允许任何有权访问服务器的人查看页面。

我们还将在所有页面上添加登录页面和注销链接。当用户被拒绝访问任何需要权限的视图时,将显示登录页面,而不是默认的“403禁止”页面。

我们将通过以下步骤实现访问控制:

  • 添加密码哈希依赖项。

  • 添加用户和组 (security.py ,一个新模块)。

  • Add a security policy (security.py).

  • 添加一个 ACL (models.py )

  • 添加 permission 向 edit_page 和 add_page 意见 (views.py )

然后我们将添加登录和注销功能:

  • 添加 login 和 logout 意见 (views.py )

  • 添加登录模板 (login.pt )

  • 使现有视图返回 logged_in 指向渲染器的标志 (views.py )

  • 添加登录和查看或编辑页面时显示的“注销”链接 (view.pt , edit.pt )

访问控制

添加依赖项

就像在 定义视图 我们需要一个新的依赖。我们需要添加 bcrypt 打包到我们的教程包 setup.py 通过将此依赖项分配给 requires 中的参数 setup() 功能。

正常开放 setup.py 并编辑如下:

11requires = [
12    'bcrypt',
13    'docutils',
14    'plaster_pastedeploy',
15    'pyramid',
16    'pyramid_chameleon',
17    'pyramid_debugtoolbar',
18    'waitress',
19    'pyramid_retry',
20    'pyramid_tm',
21    'pyramid_zodbconn',
22    'transaction',
23    'ZODB',
24]
25
26tests_require = [
27    'WebTest',
28    'pytest',
29    'pytest-cov',
30]

只需添加突出显示的行。

别忘了跑步 pip install -e . 就像在 运行 pip install -e . .

备注

我们正在使用 bcrypt 从pypi打包以安全地散列密码。如果您的系统中存在bcrypt问题,那么密码还有其他单向哈希算法。只需确保它是一个被批准用于存储密码的算法,而不是一个通用的单向散列。

添加安全策略

创建新的 tutorial/security.py 包含以下内容的模块:

 1import bcrypt
 2from pyramid.authentication import AuthTktCookieHelper
 3from pyramid.authorization import (
 4    ACLHelper,
 5    Authenticated,
 6    Everyone,
 7)
 8
 9
10def hash_password(pw):
11    hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
12    # return unicode instead of bytes because databases handle it better
13    return hashed_pw.decode('utf-8')
14
15def check_password(expected_hash, pw):
16    if expected_hash is not None:
17        return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8'))
18    return False
19
20USERS = {
21    'editor': hash_password('editor'),
22    'viewer': hash_password('viewer'),
23}
24GROUPS = {'editor': ['group:editors']}
25
26class MySecurityPolicy:
27    def __init__(self, secret):
28        self.authtkt = AuthTktCookieHelper(secret)
29        self.acl = ACLHelper()
30
31    def identity(self, request):
32        identity = self.authtkt.identify(request)
33        if identity is not None and identity['userid'] in USERS:
34            return identity
35
36    def authenticated_userid(self, request):
37        identity = self.identity(request)
38        if identity is not None:
39            return identity['userid']
40
41    def remember(self, request, userid, **kw):
42        return self.authtkt.remember(request, userid, **kw)
43
44    def forget(self, request, **kw):
45        return self.authtkt.forget(request, **kw)
46
47    def permits(self, request, context, permission):
48        principals = self.effective_principals(request)
49        return self.acl.permits(context, principals, permission)
50
51    def effective_principals(self, request):
52        principals = [Everyone]
53        identity = self.identity(request)
54        if identity is not None:
55            principals.append(Authenticated)
56            principals.append('u:' + identity['userid'])
57            principals.extend(GROUPS.get(identity['userid'], []))
58        return principals
59
60def includeme(config):
61    settings = config.get_settings()
62
63    config.set_security_policy(MySecurityPolicy(settings['auth.secret']))

因为我们添加了一个新的 tutorial/security.py 模块,我们需要包括它。打开文件 tutorial/__init__.py 并编辑以下行:

 1from pyramid.config import Configurator
 2from pyramid_zodbconn import get_connection
 3
 4from .models import appmaker
 5
 6
 7def root_factory(request):
 8    conn = get_connection(request)
 9    return appmaker(conn.root())
10
11
12def main(global_config, **settings):
13    """ This function returns a Pyramid WSGI application.
14    """
15    with Configurator(settings=settings) as config:
16        config.include('pyramid_chameleon')
17        config.include('pyramid_tm')
18        config.include('pyramid_retry')
19        config.include('pyramid_zodbconn')
20        config.include('.routes')
21        config.include('.security')
22        config.set_root_factory(root_factory)
23        config.scan()
24    return config.make_wsgi_app()

安全策略控制身份验证和授权的几个方面:

  • 识别当前用户的 identity 对于一个 request .

  • 授权访问资源。

  • 为记住和忘记用户创建有效负载。

识别登录用户

这个 MySecurityPolicy.identity 方法检查 request 并确定它是否来自经过身份验证的用户。它通过利用 pyramid.authentication.AuthTktCookieHelper 类存储 identity 在加密签名的cookie中。如果 request 包含一个标识,然后执行最终检查以确定该用户在当前 USERS 存储。

授权访问资源

这个 MySecurityPolicy.permits 方法确定 request 允许一个特定的 permission 关于给定 context . 此过程分为几个步骤:

  • 转换 request 列入清单 principals 通过 MySecurityPolicy.effective_principals 方法。

  • 将主体列表与 context 使用 pyramid.authorization.ACLHelper . 只有在找到 ACE 授予其中一位校长必要的许可。

对于我们的应用程序,我们定义了几个主体的列表:

各种wiki页面将授予其中一些主体编辑现有页面或添加新页面的权限。

最后,有两个helper方法可以帮助我们对用户进行身份验证。首先是 hash_password 它采用原始密码,并使用bcrypt将其转换为不可逆的表示形式,即所谓的“散列”过程。第二种方法, check_password ,将允许我们将提交的密码的哈希值与存储在用户记录中的密码的哈希值进行比较。如果两个哈希值匹配,则提交的密码有效,我们可以对用户进行身份验证。

我们散列密码,这样就不可能解密并使用它们在应用程序中进行身份验证。如果我们愚蠢地将密码以明文存储,那么任何有权访问数据库的人都可以检索任何密码以作为任何用户进行身份验证。

在生产系统中,用户和组数据通常会被保存并来自数据库。这里我们使用“虚拟”数据来表示用户和组源。

添加新设置

我们的身份验证策略需要一个新设置, 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 = testing-seekrit

添加ACL

正常开放 tutorial/models/__init__.py 并在顶部附近添加以下import语句:

3from pyramid.authorization import (
4    Allow,
5    Everyone,
6)

将以下行添加到 Wiki 类:

 9class Wiki(PersistentMapping):
10    __name__ = None
11    __parent__ = None
12    __acl__ = [
13        (Allow, Everyone, 'view'),
14        (Allow, 'group:editors', 'edit'),
15    ]

我们进口 Allow ,表示允许该权限的操作。我们也进口 Everyone 一种特殊的 principal 与所有请求关联的。两者都用于 ACE 组成ACL的条目。

acl是需要命名的列表 __acl__ 成为一个类的属性。我们定义了一个 ACL 用两 ACE 条目。第一个条目允许任何用户 view 许可。第二个条目允许 group:editors 校长 edit 许可。

这个 Wiki 包含acl的类是 resource 的构造函数 root 资源,它是 Wiki 实例。acl被提供给 context 请求的 context 属性。

我们只是偶然在类范围内分配了这个ACL。ACL可以附加到对象 实例 也是。这就是如何在 Pyramid 应用。我们实际上只需要 one 但是,对于整个系统,由于我们的安全需求很简单,因此不演示此功能。

参见

见 实现ACL授权 有关 ACL 代表。

添加权限声明

正常开放 tutorial/views/default.py . 添加一个 permission='view' 参数 @view_config 装饰者 view_wiki() 和 view_page() 如下:

12@view_config(context='..models.Wiki', permission='view')
17@view_config(context='..models.Page',
18             renderer='tutorial:templates/view.pt',
19             permission='view')

只需编辑和添加突出显示的行及其前面的逗号。

这允许任何人调用这两个视图。

下一步添加 permission='edit' 参数 @view_config 装饰者 add_page() 和 edit_page() :

39@view_config(name='add_page', context='..models.Wiki',
40             renderer='tutorial:templates/edit.pt',
41             permission='edit')
58@view_config(name='edit_page', context='..models.Page',
59             renderer='tutorial:templates/edit.pt',
60             permission='edit')

只需编辑和添加突出显示的行及其前面的逗号。

结果是只有拥有 edit 请求时的权限可以调用这两个视图。

我们已经完成了控制访问所需的更改。接下来的更改将添加登录和注销功能。

登录退出

添加登录和注销视图

我们将添加一个 login 显示登录表单并从登录表单处理日志的视图,检查凭据。

我们还将添加 logout 查看可调用到我们的应用程序并提供指向它的链接。此视图将清除登录用户的凭据并重定向回首页。

添加新文件 tutorial/views/auth.py 包括以下内容:

 1from pyramid.httpexceptions import HTTPSeeOther
 2from pyramid.security import (
 3    forget,
 4    remember,
 5)
 6from pyramid.view import (
 7    forbidden_view_config,
 8    view_config,
 9)
10
11from ..security import check_password, USERS
12
13
14@view_config(context='..models.Wiki', name='login',
15             renderer='tutorial:templates/login.pt')
16@forbidden_view_config(renderer='tutorial:templates/login.pt')
17def login(request):
18    login_url = request.resource_url(request.root, 'login')
19    referrer = request.url
20    if referrer == login_url:
21        referrer = '/'  # never use the login form itself as came_from
22    came_from = request.params.get('came_from', referrer)
23    message = ''
24    login = ''
25    password = ''
26    if 'form.submitted' in request.params:
27        login = request.params['login']
28        password = request.params['password']
29        if check_password(USERS.get(login), password):
30            headers = remember(request, login)
31            return HTTPSeeOther(location=came_from, headers=headers)
32        message = 'Failed login'
33        request.response.status = 400
34
35    return dict(
36        message=message,
37        url=login_url,
38        came_from=came_from,
39        login=login,
40        password=password,
41        title='Login',
42    )
43
44
45@view_config(context='..models.Wiki', name='logout')
46def logout(request):
47    headers = forget(request)
48    return HTTPSeeOther(
49        location=request.resource_url(request.context),
50        headers=headers,
51    )

forbidden_view_config() 将用于自定义默认403禁止页面。 remember() 和 forget() 帮助创建身份验证票证cookie并使其过期。

login() 有两个装饰师:

  • A @view_config 将它与 login 路线并在我们访问时使其可见 /login .

  • A @forbidden_view_config 把它变成一个 forbidden view . login() 当用户试图执行一个视图可调用时,将调用该视图,而该视图可调用的用户没有授权。例如,如果用户尚未登录并尝试添加或编辑wiki页面,则在允许继续之前,将显示登录表单。

这两个的顺序 view configuration 装饰师不重要。

logout() 装饰有 @view_config 将它与 logout 路线。它将在我们访问时调用 /logout .

添加 login.pt 模板

创造 tutorial/templates/login.pt with the following content:

<div metal:use-macro="load: layout.pt">
    <div metal:fill-slot="content">

        <div class="content">
              <p>
                <strong>
                  Login
                </strong><br>
                <span tal:replace="message"></span>
              </p>
              <form action="${url}" method="post">
                <input type="hidden" name="came_from" value="${came_from}">
                <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" value="${password}">
                </div>
                <div class="form-group">
                  <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
                </div>
              </form>
        </div>

    </div>
</div>

上面的模板在我们刚刚添加的登录视图中被引用 views.py .

在浏览器中查看应用程序

我们最终可以在浏览器中检查我们的应用程序(请参见 启动应用程序 )启动浏览器并访问以下每个URL,检查结果是否符合预期:

  • http://localhost:6543/ invokes the view_wiki 查看。这总是重定向到 view_page 视图 FrontPage 页面资源。它可由任何用户执行。

  • http://localhost:6543/login invokes the login 视图,将显示一个登录表单。在每个页面上,当用户未通过身份验证时,在右上角有一个“Login”链接,否则,当用户通过身份验证时,它是一个“Logout”链接。

    为凭据提供用户名 editor 和密码 editor 将对用户进行身份验证并授予该组访问权限。

    登录后(点击编辑或添加页面并提交有效凭据),我们将在右上角看到“注销”链接。当我们点击它时,我们被注销,重定向回首页,“登录”链接显示在右上角。

  • http://localhost:6543/FrontPage invokes the view_page 视图 FrontPage 页面资源。这是因为它是 default view (一个没有 name 为 Page 资源。它可由任何用户执行。

  • http://localhost:6543/FrontPage/edit_page invokes the edit view for the FrontPage object. It is executable by only the editor 用户。如果不同的用户(或匿名用户)调用它,则会显示一个登录表单。这个 editor 用户将看到编辑页面表单。

  • http://localhost:6543/add_page/SomePageName invokes the add view for a page. It is executable by only the editor 用户。如果不同的用户(或匿名用户)调用它,将显示一个登录表单。这个 editor 用户将看到编辑页面表单。

  • 要生成未找到的错误,请访问http://localhost:6543/wakawaka,它将调用 notfound_view 由CookiCutter提供的视图。