20:使用身份验证登录

根据用户列表验证用户名和密码的登录视图。

背景

大多数Web应用程序都有允许人们通过Web浏览器添加/编辑/删除内容的URL。添加时间 security 应用程序。在第一步中,我们将介绍身份验证。也就是说,登录和注销,使用Pyramid丰富的可插入用户存储设施。

在下一步中,我们将使用授权安全声明介绍对资源的保护。

目标

  • 介绍认证的Pyramid概念。

  • 创建登录和注销视图。

步骤

  1. 我们将使用视图类步骤作为起点:

    cd ..; cp -r view_classes authentication; cd authentication
    
  2. 添加 bcrypt 作为一种依附 authentication/setup.py

     1from setuptools import setup
     2
     3# List of dependencies installed via `pip install -e .`
     4# by virtue of the Setuptools `install_requires` value below.
     5requires = [
     6    'bcrypt',
     7    'pyramid',
     8    'pyramid_chameleon',
     9    'waitress',
    10]
    11
    12# List of dependencies installed via `pip install -e ".[dev]"`
    13# by virtue of the Setuptools `extras_require` value in the Python
    14# dictionary below.
    15dev_requires = [
    16    'pyramid_debugtoolbar',
    17    'pytest',
    18    'webtest',
    19]
    20
    21setup(
    22    name='tutorial',
    23    install_requires=requires,
    24    extras_require={
    25        'dev': dev_requires,
    26    },
    27    entry_points={
    28        'paste.app_factory': [
    29            'main = tutorial:main'
    30        ],
    31    },
    32)
    
  3. 我们现在可以在开发模式下安装我们的项目:

    $VENV/bin/pip install -e .
    
  4. 将安全哈希放入 authentication/development.ini 配置文件为 tutorial.secret 而不是将其放入代码中:

     1[app:main]
     2use = egg:tutorial
     3pyramid.reload_templates = true
     4pyramid.includes =
     5    pyramid_debugtoolbar
     6tutorial.secret = 98zd
     7
     8[server:main]
     9use = egg:waitress#main
    10listen = localhost:6543
    
  5. 创建一个 authentication/tutorial/security.py 模块,它可以通过提供 security policy

     1import bcrypt
     2from pyramid.authentication import AuthTktCookieHelper
     3
     4
     5def hash_password(pw):
     6    pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
     7    return pwhash.decode('utf8')
     8
     9def check_password(pw, hashed_pw):
    10    expected_hash = hashed_pw.encode('utf8')
    11    return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
    12
    13
    14USERS = {'editor': hash_password('editor'),
    15         'viewer': hash_password('viewer')}
    16
    17
    18class SecurityPolicy:
    19    def __init__(self, secret):
    20        self.authtkt = AuthTktCookieHelper(secret=secret)
    21
    22    def identity(self, request):
    23        identity = self.authtkt.identify(request)
    24        if identity is not None and identity['userid'] in USERS:
    25            return identity
    26
    27    def authenticated_userid(self, request):
    28        identity = self.identity(request)
    29        if identity is not None:
    30            return identity['userid']
    31
    32    def remember(self, request, userid, **kw):
    33        return self.authtkt.remember(request, userid, **kw)
    34
    35    def forget(self, request, **kw):
    36        return self.authtkt.forget(request, **kw)
    
  6. 注册 SecurityPolicyconfigurator 在里面 authentication/tutorial/__init__.py

     1from pyramid.config import Configurator
     2
     3from .security import SecurityPolicy
     4
     5
     6def main(global_config, **settings):
     7    config = Configurator(settings=settings)
     8    config.include('pyramid_chameleon')
     9
    10    config.set_security_policy(
    11        SecurityPolicy(
    12            secret=settings['tutorial.secret'],
    13        ),
    14    )
    15
    16    config.add_route('home', '/')
    17    config.add_route('hello', '/howdy')
    18    config.add_route('login', '/login')
    19    config.add_route('logout', '/logout')
    20    config.scan('.views')
    21    return config.make_wsgi_app()
    
  7. 更新中的视图 authentication/tutorial/views.py

     1from pyramid.httpexceptions import HTTPFound
     2from pyramid.security import (
     3    remember,
     4    forget,
     5    )
     6
     7from pyramid.view import (
     8    view_config,
     9    view_defaults
    10    )
    11
    12from .security import (
    13    USERS,
    14    check_password
    15)
    16
    17
    18@view_defaults(renderer='home.pt')
    19class TutorialViews:
    20    def __init__(self, request):
    21        self.request = request
    22        self.logged_in = request.authenticated_userid
    23
    24    @view_config(route_name='home')
    25    def home(self):
    26        return {'name': 'Home View'}
    27
    28    @view_config(route_name='hello')
    29    def hello(self):
    30        return {'name': 'Hello View'}
    31
    32    @view_config(route_name='login', renderer='login.pt')
    33    def login(self):
    34        request = self.request
    35        login_url = request.route_url('login')
    36        referrer = request.url
    37        if referrer == login_url:
    38            referrer = '/'  # never use login form itself as came_from
    39        came_from = request.params.get('came_from', referrer)
    40        message = ''
    41        login = ''
    42        password = ''
    43        if 'form.submitted' in request.params:
    44            login = request.params['login']
    45            password = request.params['password']
    46            hashed_pw = USERS.get(login)
    47            if hashed_pw and check_password(password, hashed_pw):
    48                headers = remember(request, login)
    49                return HTTPFound(location=came_from,
    50                                 headers=headers)
    51            message = 'Failed login'
    52
    53        return dict(
    54            name='Login',
    55            message=message,
    56            url=request.application_url + '/login',
    57            came_from=came_from,
    58            login=login,
    59            password=password,
    60        )
    61
    62    @view_config(route_name='logout')
    63    def logout(self):
    64        request = self.request
    65        headers = forget(request)
    66        url = request.route_url('home')
    67        return HTTPFound(location=url,
    68                         headers=headers)
    
  8. 在添加登录模板 authentication/tutorial/login.pt

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <title>Quick Tutorial: ${name}</title>
     5</head>
     6<body>
     7<h1>Login</h1>
     8<span tal:replace="message"/>
     9
    10<form action="${url}" method="post">
    11    <input type="hidden" name="came_from"
    12           value="${came_from}"/>
    13    <label for="login">Username</label>
    14    <input type="text" id="login"
    15           name="login"
    16           value="${login}"/><br/>
    17    <label for="password">Password</label>
    18    <input type="password" id="password"
    19           name="password"
    20           value="${password}"/><br/>
    21    <input type="submit" name="form.submitted"
    22           value="Log In"/>
    23</form>
    24</body>
    25</html>
    
  9. 在中提供登录/注销框 authentication/tutorial/home.pt

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <title>Quick Tutorial: ${name}</title>
     5</head>
     6<body>
     7
     8<div>
     9    <a tal:condition="view.logged_in is None"
    10            href="${request.application_url}/login">Log In</a>
    11    <a tal:condition="view.logged_in is not None"
    12            href="${request.application_url}/logout">Logout</a>
    13</div>
    14
    15<h1>Hi ${name}</h1>
    16<p>Visit <a href="${request.route_url('hello')}">hello</a></p>
    17</body>
    18</html>
    
  10. 运行 Pyramid 应用程序时使用:

    $VENV/bin/pserve development.ini --reload
    
  11. 在浏览器中打开http://localhost:6543/。

  12. 单击“登录”链接。

  13. 使用用户名提交登录表单 editor 以及密码 editor .

  14. 请注意,“登录”链接已更改为“注销”。

  15. 单击“注销”链接。

分析

与许多web框架不同,Pyramid包含一个内置但可选的身份验证和授权安全模型。该安全系统旨在灵活并支持多种需求。在这个安全模型中,身份验证(您是谁)和授权(允许您做什么)是可插入的。为了一步一个脚印地学习,我们提供了一个识别用户并允许他们注销的系统。

在这个例子中,我们选择使用捆绑 pyramid.authentication.AuthTktCookieHelper 帮助程序将用户的登录状态存储在cookie中。我们在配置中启用了它,并在INI文件中提供了票证签名机密。

我们的视图类增加了一个登录视图。当你通过一个 GET 请求,它返回了一个登录表单。到达时通过 POST ,它根据 USERS 数据存储。

函数 hash_password 使用单向哈希算法,在用户密码上添加salt,通过 bcrypt 而不是以纯文本形式存储密码。这被认为是安全方面的“最佳实践”。

备注

还有其他的类库 bcrypt 如果是系统问题。只要确保库使用了一个被批准的安全存储密码的算法。

函数 check_password 将比较提交的密码和存储在数据库中的用户密码的两个哈希值。如果散列值是等效的,那么将对用户进行身份验证,否则身份验证将失败。

假设密码被验证,我们调用 pyramid.security.remember() 生成在响应中设置的cookie。随后的请求返回该cookie并标识用户。

在我们的模板中,我们获取了 logged_in 视图类中的值。我们使用这个来计算登录用户(如果有的话)。在模板中,我们可以选择显示匿名访问者的登录链接或登录用户的注销链接。

额外credit

  1. 我可以用数据库代替吗 USERS 对用户进行身份验证?

  2. 一旦我登录,是否有任何以用户为中心的信息被阻塞到每个请求上?使用 import pdb; pdb.set_trace() 回答这个问题。