休息账户管理

请注意

本教程假设您已经阅读了 快速启动 以及 认证和授权 指南。

除了相对罕见的开放(通常为只读)公共API之外,大多数服务只能由经过身份验证的用户访问。一种常见的模式是,用户在网站上或使用移动应用程序创建他们的帐户。一旦他们有了账户,他们就可以消费一个或多个API。这是大多数社交网络和服务提供商(Twitter、Facebook、Netflix等)遵循的模式。那么,作为服务提供商,您如何在使用帐户本身使用的相同API的同时创建、编辑和删除帐户?

在下面的段落中,我们将看到一些可能的帐户管理实现,它们都充分利用了大量的EVE功能,例如 自定义终结点安全性基于角色的访问控制用户限制的资源访问事件钩子 .

我们假设启用了ssl/tls,这意味着我们的传输层是加密的,使得 基本身份验证基于令牌的身份验证 保护API端点的有效选项。

假设我们正在升级在 快速启动 辅导的。

具有基本身份验证的帐户

我们的任务如下:

  1. 使终结点可用于所有帐户管理活动 (/accounts

  2. 确保端点安全,以便只有我们控制的客户机才能访问端点:我们自己的网站、具有帐户管理功能的移动应用程序等。

  3. 确保所有其他API端点都只能由经过身份验证的帐户访问(通过上述端点创建)。

  4. 允许经过身份验证的用户仅访问自己创建的资源。

1.这个 /accounts 端点

帐户管理终结点与任何其他API终结点相同。这只是在设置文件中声明的问题。让我们先声明资源模式。

schema =  {
    'username': {
        'type': 'string',
        'required': True,
        'unique': True,
        },
    'password': {
        'type': 'string',
        'required': True,
    },
},

然后,让我们定义端点。

accounts = {
    # the standard account entry point is defined as
    # '/accounts/<ObjectId>'. We define  an additional read-only entry
    # point accessible at '/accounts/<username>'.
    'additional_lookup': {
        'url': 'regex("[\w]+")',
        'field': 'username',
    },

    # We also disable endpoint caching as we don't want client apps to
    # cache account data.
    'cache_control': '',
    'cache_expires': 0,

    # Finally, let's add the schema definition for this endpoint.
    'schema': schema,
}

我们在 /accounts/<username> .这不是真正的必要性,但它可以方便地验证是否已经使用了用户名,或者在不知道用户名的情况下检索帐户。 ObjectId 事先。当然,这两条信息也可以通过查询资源端点来找到。 (/accounts?where={{"username": "johndoe"}} ,但是随后我们需要解析响应负载,而通过使用get请求访问我们的新端点,我们将获得裸帐户数据,或者 404 Not Found 如果帐户不存在。

配置完端点后,我们需要将其添加到API域:

DOMAIN['accounts'] = accounts

2.第二步。确保 /accounts/ 端点

2a.硬编码

通过只允许众所周知的 superusers 操作它。我们的身份验证类(在启动脚本中定义)可以硬编码来处理这种情况:

import bcrypt
from eve import Eve
from eve.auth import BasicAuth


class BCryptAuth(BasicAuth):
    def check_auth(self, username, password, allowed_roles, resource, method):
        if resource == 'accounts':
            return username == 'superuser' and password == 'password'
        else:
            # use Eve's own db driver; no additional connections/resources are used
            accounts = app.data.driver.db['accounts']
            account = accounts.find_one({'username': username})
            return account and \
                bcrypt.hashpw(password, account['password']) == account['password']


if __name__ == '__main__':
    app = Eve(auth=BCryptAuth)
    app.run()

因此,只有 superuser 帐户将被允许使用 accounts 端点,而标准身份验证逻辑将应用于所有其他端点。我们的移动应用程序(比如说)将通过简单的POST请求访问端点来添加帐户,当然,将自己认证为 superuser 通过 Authorization 标题。脚本假定存储的密码是用 bcrypt (将密码存储为纯文本 从未 一个好主意)。参见 基本身份验证 另一种选择是,更快但不太安全的sha1/mac示例。

2b.用户角色访问控制

硬编码用户名和密码可能很好地完成了这项工作,但这并不是我们在这里可以采用的最佳方法。如果另一个呢 superurser 帐户需要访问终结点吗?每次特权用户加入列组时更新脚本似乎不合适(不是)。幸运的是, 基于角色的访问控制 功能可以帮助我们。你知道我们要去哪儿了吗:我们的想法是只有 superuseradmin 将授予角色访问端点的权限。

让我们从更新我们的资源模式开始。

     schema =  {
         'username': {
             'type': 'string',
             'required': True,
             },
         'password': {
             'type': 'string',
             'required': True,
         },
         'roles': {
             'type': 'list',
             'allowed': ['user', 'superuser', 'admin'],
             'required': True,
         }
     },

我们刚添加了一个新的 roles 字段,它是必需的列表。从现在起,必须在创建帐户时分配一个或多个角色。

现在我们需要限制端点访问 superuseradmin 只有帐户,我们才能相应地更新端点定义。

 accounts = {
     # the standard account entry point is defined as
     # '/accounts/<ObjectId>'. We define  an additional read-only entry
     # point accessible at '/accounts/<username>'.
     'additional_lookup': {
         'url': 'regex("[\w]+")',
         'field': 'username',
     },

     # We also disable endpoint caching as we don't want client apps to
     # cache account data.
     'cache_control': '',
     'cache_expires': 0,

     # Only allow superusers and admins.
     'allowed_roles': ['superuser', 'admin'],

     # Finally, let's add the schema definition for this endpoint.
     'schema': schema,
 }

最后,重写我们的身份验证类是正确的。

from eve import Eve
from eve.auth import BasicAuth
from werkzeug.security import check_password_hash


class RolesAuth(BasicAuth):
    def check_auth(self, username, password, allowed_roles, resource, method):
        # use Eve's own db driver; no additional connections/resources are used
        accounts = app.data.driver.db['accounts']
        lookup = {'username': username}
        if allowed_roles:
            # only retrieve a user if his roles match ``allowed_roles``
            lookup['roles'] = {'$in': allowed_roles}
        account = accounts.find_one(lookup)
        return account and check_password_hash(account['password'], password)


if __name__ == '__main__':
    app = Eve(auth=RolesAuth)
    app.run()

上面的代码段所做的是通过角色库访问控制来保护所有API端点。事实上,它与 基于角色的访问控制 .这种技术允许我们在添加更多代码时保持代码不变 superuseradmin 帐户(我们可能会通过访问我们自己的API添加它们)。此外,如果出现需要,我们可以通过更新设置文件轻松地限制对更多端点的访问,而无需接触身份验证类。

三。保护其他API端点

这很快,因为 hard-coding 以及 role-based 上述访问控制方法已经有效地保护了所有API端点。将身份验证类传递给 Eve 对象为整个API启用身份验证:每次请求命中端点时,都会调用类实例。

当然,您仍然可以微调安全性,例如允许公共访问某些端点或某些HTTP方法。参见 认证和授权 了解更多详细信息。

第四章。仅允许访问帐户资源

大多数情况下,当您允许经过身份验证的用户存储数据时,您只希望他们访问自己的数据。这可以通过使用 用户限制的资源访问 功能。启用后,每个存储的文档都与创建它的帐户关联。这允许API在所有类型的请求上透明地只提供帐户创建的文档:读取、编辑、删除,当然还有创建。

只有两件事我们需要做才能激活此功能:

  1. 配置将用于存储文档所有者的字段的名称;

  2. 对每个传入的日志请求设置文档所有者。

因为我们想要为所有的API端点启用这个特性,我们只需要更新我们的 settings.py 通过设置适当的 AUTH_FIELD 价值观:

# Name of the field used to store the owner of each document
AUTH_FIELD = 'user_id'

然后,我们要更新我们的身份验证类以正确更新字段的值:

 from eve import Eve
 from eve.auth import BasicAuth
 from werkzeug.security import check_password_hash


 class RolesAuth(BasicAuth):
     def check_auth(self, username, password, allowed_roles, resource, method):
         # use Eve's own db driver; no additional connections/resources are used
         accounts = app.data.driver.db['accounts']
         lookup = {'username': username}
         if allowed_roles:
             # only retrieve a user if his roles match ``allowed_roles``
             lookup['roles'] = {'$in': allowed_roles}
         account = accounts.find_one(lookup)
         # set 'AUTH_FIELD' value to the account's ObjectId
         # (instead of _Id, you might want to use ID_FIELD)
         self.set_request_auth_value(account['_id'])
         return account and check_password_hash(account['password'], password)


 if __name__ == '__main__':
     app = Eve(auth=RolesAuth)
     app.run()

这就是我们要做的。现在,当客户点击时, /invoices 带有GET请求的端点,将只向其自己的帐户创建的发票提供服务。同样的情况也会发生在删除和修补程序中,使得经过身份验证的用户不可能意外地检索、编辑或删除其他人的数据。

具有令牌身份验证的帐户

如中所示 基于令牌的身份验证 ,令牌身份验证只是基本身份验证的专用版本。它实际上是作为标准的基本身份验证请求执行的,其中 用户名 字段用于令牌,不提供密码字段(如果包含,则忽略该字段)。

因此,使用令牌身份验证处理帐户与我们在中看到的非常相似 具有基本身份验证的帐户 但是有一点需要注意:令牌需要与帐户一起生成和存储,并最终返回给客户机。

鉴于此,让我们回顾一下更新后的任务列表:

  1. 使终结点可用于所有帐户管理活动 (/accounts

  2. 保护端点,以便它只能被我们控制的客户机(令牌)访问。

  3. 创建帐户时,生成并存储其令牌。

  4. 或者,返回带有响应的新令牌。

  5. 确保所有其他API端点只能访问经过身份验证的令牌。

  6. 允许经过身份验证的用户仅访问自己创建的资源

1.这个 /accounts/ 端点

这和我们以前做的没什么不同 具有基本身份验证的帐户 .我们只需要添加 token 模式字段:

     schema =  {
         'username': {
             'type': 'string',
             'required': True,
             'unique': True,
             },
         'password': {
             'type': 'string',
             'required': True,
         },
         'roles': {
             'type': 'list',
             'allowed': ['user', 'superuser', 'admin'],
             'required': True,
         },
         'token': {
             'type': 'string',
             'required': True,
         }
     }

2.第二步。确保 /accounts/ 端点

我们定义了 roles 字段为 accounts 上一步中的架构。我们还需要定义端点,确保我们设置了允许的用户角色。

 accounts = {
     # the standard account entry point is defined as
     # '/accounts/<ObjectId>'. We define  an additional read-only entry
     # point accessible at '/accounts/<username>'.
     'additional_lookup': {
         'url': 'regex("[\w]+")',
         'field': 'username',
     },

     # We also disable endpoint caching as we don't want client apps to
     # cache account data.
     'cache_control': '',
     'cache_expires': 0,

     # Only allow superusers and admins.
     'allowed_roles': ['superuser', 'admin'],

     # Finally, let's add the schema definition for this endpoint.
     'schema': schema,
 }

最后,这里是我们的启动脚本,当然,它使用 TokenAuth 这一次的子类是:

from eve import Eve
from eve.auth import TokenAuth


class RolesAuth(TokenAuth):
    def check_auth(self, token,  allowed_roles, resource, method):
        # use Eve's own db driver; no additional connections/resources are used
        accounts = app.data.driver.db['accounts']
        lookup = {'token': token}
        if allowed_roles:
            # only retrieve a user if his roles match ``allowed_roles``
            lookup['roles'] = {'$in': allowed_roles}
        account = accounts.find_one(lookup)
        return account


if __name__ == '__main__':
    app = Eve(auth=RolesAuth)
    app.run()

三。创建帐户时生成自定义令牌

上面的代码有一个问题:它不会验证任何人,因为我们还没有生成任何令牌。因此,客户机无法取回他们的身份验证令牌,因此他们不知道如何进行身份验证。让我们用Awesome来解决这个问题 事件钩子 功能。我们将通过注册一个回调函数来更新启动脚本,该函数将在新帐户即将存储到数据库时调用。

 from eve import Eve
 from eve.auth import TokenAuth
 import random
 import string


 class RolesAuth(TokenAuth):
     def check_auth(self, token,  allowed_roles, resource, method):
         # use Eve's own db driver; no additional connections/resources are used
         accounts = app.data.driver.db['accounts']
         lookup = {'token': token}
         if allowed_roles:
             # only retrieve a user if his roles match ``allowed_roles``
             lookup['roles'] = {'$in': allowed_roles}
         account = accounts.find_one(lookup)
         return account


 def add_token(documents):
     # Don't use this in production:
     # You should at least make sure that the token is unique.
     for document in documents:
         document["token"] = (''.join(random.choice(string.ascii_uppercase)
                                      for x in range(10)))


 if __name__ == '__main__':
     app = Eve(auth=RolesAuth)
     app.on_insert_accounts += add_token
     app.run()

如您所见,我们正在订阅 on_insert 事件 accounts endpoint with our add_token function. This callback will receive documents as an argument, which is a list of validated documents accepted for database insertion. We simply add (or replace in the unlikely case that the request contained it already) a token to every document, and we're done! For more information on callbacks, see Event Hooks .

第四章。返回带有响应的令牌

或者,您可能希望返回带有响应的令牌。说实话,这不是个好主意。您通常希望在带外发送访问信息,例如电子邮件。但是,我们假设我们使用的是SSL,在某些情况下,发送auth令牌是有意义的,比如当客户端是移动应用程序,我们希望用户立即使用该服务。

通常,只有自动处理的字段 (ID_FIELDLAST_UPDATEDDATE_CREATEDETAG )包含在响应后有效载荷中。幸运的是,有一个设置允许我们在响应中注入额外的字段,也就是说 EXTRA_RESPONSE_FIELDS ,其端点级别等效, extra_response_fields .我们需要做的就是相应地更新端点定义:

 accounts = {
     # the standard account entry point is defined as
     # '/accounts/<ObjectId>'. We define  an additional read-only entry
     # point accessible at '/accounts/<username>'.
     'additional_lookup': {
         'url': 'regex("[\w]+")',
         'field': 'username',
     },

     # We also disable endpoint caching as we don't want client apps to
     # cache account data.
     'cache_control': '',
     'cache_expires': 0,

     # Only allow superusers and admins.
     'allowed_roles': ['superuser', 'admin'],

     # Allow 'token' to be returned with POST responses
     'extra_response_fields': ['token'],

     # Finally, let's add the schema definition for this endpoint.
     'schema': schema,
 }

从现在开始,对针对 /accounts 端点将包括新生成的身份验证令牌,允许客户端立即使用其他API端点。

5.保护其他API端点

如前所述,将身份验证类传递给 Eve 对象为所有API端点启用身份验证。同样,您仍然可以通过允许公共访问某些端点或某些HTTP方法来微调安全性。参见 认证和授权 了解更多详细信息。

6.仅允许访问帐户资源

这是通过 用户限制的资源访问 功能,如中所示 具有基本身份验证的帐户 .您可能希望将用户令牌存储为 AUTH_FIELD 值,但是如果您希望用户令牌易于撤销,那么您的最佳选择是为此使用帐户唯一ID。

基本vs令牌:最终考虑

尽管在服务器端设置起来有点困难,但是令牌身份验证提供了显著的优势。首先,您没有将密码存储在客户机上,并且在每次请求时都通过网络发送密码。如果您将令牌发送到带外,并且使用的是SSL/TLS,那么这就增加了相当多的安全性。