Cred:可插拔身份验证

目标

Cred 是一个用于服务器的可插拔身份验证系统。它允许任意数量的网络协议连接并验证到系统,并与系统中对特定协议有意义的方面进行通信。例如,Twisted 的 POP3 支持传递一组“用户名和密码”凭据以获取指定电子邮件帐户的邮箱。IMAP 也是如此,但它检索同一邮箱的略微不同的视图,从而启用 IMAP 中特定于 IMAP 的功能,而这些功能在其他邮件协议中不可用。

Cred 的设计目的是允许在部署期间决定业务逻辑的后端实现(称为化身)和身份验证数据库(称为凭据检查器)。例如,同一个 POP3 服务器应该能够针对本地 UNIX 密码数据库或 LDAP 服务器进行身份验证,而无需了解邮件存储方式或位置。

为了概述其工作原理,一个“领域”对应于一个应用程序域,并负责化身,化身是可通过网络访问的业务逻辑对象。为了将它连接到身份验证数据库,一个名为Portal 的顶级对象存储一个领域和多个凭据检查器。想要登录的任何东西,例如一个Protocol,都存储对门户的引用。登录包括将凭据和请求接口(例如 POP3 的IMailboxPOP3)传递给门户。门户将凭据传递给相应的凭据检查器,凭据检查器返回一个化身 ID。该 ID 被传递给领域,领域返回相应的化身。对于具有创建邮箱对象的领域的 Portal 和检查 /etc/passwd 的凭据检查器,登录包括将用户名/密码和 IMailboxPOP3 接口传递给门户。门户将此传递给 /etc/passwd 凭据检查器,获取与电子邮件帐户相对应的化身 ID,将其传递给领域,并获取该电子邮件帐户的邮箱对象。

将所有这些放在一起,以下是如何处理登录请求的典型流程

../../_images/cred-login.png

Cred 对象

门户

这是登录的核心,是 Cred 系统中所有对象之间的集成点。Portal 只有一个具体实现,没有接口 - 它执行一个非常简单的任务。一个Portal 将一个 (1) 领域与一组 CredentialChecker 实例相关联。(稍后将详细介绍这些实例。)

如果您正在编写需要针对某些内容进行身份验证的协议,则需要引用 Portal,而不需要引用其他任何内容。它只有 2 个方法 -

  • login (credentials, mind, *interfaces)

    文档字符串非常详细(参见twisted.cred.portal),但简而言之,这就是您需要调用以将用户连接到系统的方法。通常您只传递一个接口,并且 mind 为 None。接口是返回的化身预期实现的可能接口,按优先级排序。结果是一个 Deferred,它触发一个包含以下内容的元组:

    • 化身实现的接口(它是传递给 *interfaces 元组的接口之一)

    • 实现该接口的对象(化身)

    • logout,一个 0 个参数的可调用对象,它断开此登录调用建立的连接

    当化身注销时,必须调用 logout 方法。对于 POP3,这意味着在协议断开连接或注销时,等等。

  • registerChecker (checker, *credentialInterfaces)

    它将 CredentialChecker 添加到门户。可选的接口列表是检查器能够检查的凭据的接口。

凭据检查器

这是一个实现ICredentialsChecker 的对象,它将一些凭据解析为化身 ID。

无论凭据存储在内存数据结构、Apache 风格的 htaccess 文件、UNIX 密码数据库、SSH 密钥数据库还是任何其他形式中,ICredentialsChecker 的实现都是将这些数据连接到 Cred 的方式。

凭据检查器通过指定一个 credentialInterfaces 属性来规定其可以检查的凭据的一些要求,该属性是一个接口列表。传递给其 requestAvatarId 方法的凭据必须实现这些接口之一。

在大多数情况下,这些东西只会检查用户名和密码并生成用户名作为结果,但希望我们很快就能看到一些基于公钥、质询-响应和证书的凭据检查器机制。

如果凭据检查器无法验证用户,则应引发错误,并返回 twisted.cred.checkers.ANONYMOUS 以进行匿名访问。

凭据

奇怪的是,它代表用户提供的某些凭据。通常这只是一个小的静态数据块,但在某些情况下,它实际上是一个连接到网络协议的对象。例如,用户名/密码对是静态的,但质询/响应服务器是一个活动的有限状态机,需要多次方法调用才能确定结果。

Twisted 在twisted.cred.credentials 模块中提供了一些凭据接口和实现,例如IUsernamePasswordIUsernameHashedPassword

领域

领域是一个接口,它将您的“业务对象”宇宙连接到身份验证系统。

IRealm 是另一个单方法接口

  • requestAvatar (avatarId, mind, *interfaces)

    此方法通常从“Portal.login”调用。avatarId 是由 CredentialChecker 返回的。

    注意

    请注意,avatarId 必须始终是字符串。特别是,不要使用 Unicode 字符串。如果需要国际化支持,建议使用 UTF-8,并在领域中处理解码。

    关于此方法,重要的是要认识到,如果它被调用,用户已经通过身份验证。因此,如果可能,领域应该在可能的情况下创建一个新用户(如果不存在)。当然,有时在没有更多信息的情况下,这将是不可能的,这就是接口参数的作用。

由于 requestAvatar 应该从 Deferred 回调中调用,因此它可能返回一个 Deferred 或同步结果。

头像

头像是一个特定用户的业务逻辑对象。对于 POP3,它是一个邮箱,对于第一人称射击游戏,它是与游戏交互的对象,即演员。头像特定于应用程序,每个头像代表一个唯一的“用户”。

思维

如前所述,思维通常是 None,因此如果您愿意,可以跳过此部分。

Perspective Broker 的大师们已经知道这个对象是错误命名的“客户端对象”。没有“思维”类,甚至没有接口,但它是一个发挥重要作用的对象 - 任何要转发给已认证客户端的通知都将通过“思维”传递。此外,它允许在登录期间除了头像 ID 之外,将更多信息传递给领域。

这个名字可能看起来很不寻常,但考虑到思维代表网络连接“另一端”的实体,它既接收更新又发出命令,我认为它是合适的。

虽然许多协议不会使用它,但它起着重要的作用。它作为 Portal 和 Realm 的参数提供,尽管 CredentialChecker 应该仅通过 Credentials 实例与客户端程序交互。

与原始的 Perspective Broker “客户端对象”不同,思维的实现通常由连接的协议而不是 Realm 决定。需要特定接口来发出通知的 Realm 需要用适配器包装协议的思维实现,以获得符合其预期接口的实现 - 然而,Perspective Broker 可能会继续使用客户端对象具有预先指定远程接口的模型。

(如果您不太理解这一点,没关系。它很难解释,而且在 cred 的简单用法中没有使用,因此您可以随意传递 None,直到您发现自己需要类似的东西。)

职责

服务器协议实现

协议实现者应该定义头像应该实现的接口,并将协议设计为具有附加的 Portal。当用户使用协议登录时,将创建一个凭据对象,传递给 Portal,并请求具有适当接口的头像。当用户注销或协议断开连接时,应该注销头像。

协议设计者不应硬编码用户的身份验证方式或实现的领域。例如,POP3 协议实现需要一个 Portal,其领域返回实现 IMailbox 的头像,并且其凭据检查器接受用户名/密码凭据,但仅此而已。以下是代码可能的外观草图 - 请注意,USER 和 PASS 是用于登录的协议命令,而 DELE 命令只能在您登录后使用

pop3_server.py

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from zope.interface import Interface

from twisted.cred import credentials, error
from twisted.internet import defer
from twisted.protocols import basic
from twisted.python import log


class IMailbox(Interface):
    """
    Interface specification for mailbox.
    """

    def deleteMessage(index):
        pass


class POP3(basic.LineReceiver):
    # ...
    def __init__(self, portal):
        self.portal = portal

    def do_DELE(self, i):
        # uses self.mbox, which is set after login
        i = int(i) - 1
        self.mbox.deleteMessage(i)
        self.successResponse()

    def do_USER(self, user):
        self._userIs = user
        self.successResponse("USER accepted, send PASS")

    def do_PASS(self, password):
        if self._userIs is None:
            self.failResponse("USER required before PASS")
            return
        user = self._userIs
        self._userIs = None
        d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
        d.addCallback(self._cbMailbox, user)

    def authenticateUserPASS(self, user, password):
        if self.portal is not None:
            return self.portal.login(
                credentials.UsernamePassword(user, password), None, IMailbox
            )
        raise error.UnauthorizedLogin()

    def _cbMailbox(self, ial, user):
        interface, avatar, logout = ial

        if interface is not IMailbox:
            self.failResponse("Authentication failed")
            log.err("_cbMailbox() called with an interface other than IMailbox")
            return

        self.mbox = avatar
        self._onLogout = logout
        self.successResponse("Authentication succeeded")
        log.msg("Authenticated login for " + user)

应用程序实现

应用程序开发人员可以实现领域和凭据检查器。例如,他们可能会实现一个领域,该领域返回实现 IMailbox 的头像,使用 MySQL 进行存储,或者可能是一个使用 LDAP 进行身份验证的凭据检查器。在以下示例中,为简单的远程对象服务(使用 Twisted 的 Perspective Broker 协议)实现了 Realm

from zope.interface import implementer

from twisted.spread import pb
from twisted.cred.portal import IRealm

class SimplePerspective(pb.Avatar):

    def perspective_echo(self, text):
        print('echoing',text)
        return text

    def logout(self):
        print(self, "logged out")


@implementer(IRealm)
class SimpleRealm:

    def requestAvatar(self, avatarId, mind, *interfaces):
        if pb.IPerspective in interfaces:
            avatar = SimplePerspective()
            return pb.IPerspective, avatar, avatar.logout
        else:
            raise NotImplementedError("no interface")

部署

部署涉及将协议、适当的领域和凭据检查器绑定在一起。例如,POP3 服务器可以通过附加一个 Portal 来构建,该 Portal 包装基于 MySQL 的领域和 /etc/passwd 凭据检查器,或者如果更有用,可能是 LDAP 凭据检查器。以下示例显示了如何使用内存中凭据检查器部署上一个示例中的 SimpleRealm

from twisted.spread import pb
from twisted.internet import reactor
from twisted.cred.portal import Portal
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse

portal = Portal(SimpleRealm())
checker = InMemoryUsernamePasswordDatabaseDontUse()
checker.addUser("guest", "password")
portal.registerChecker(checker)
reactor.listenTCP(9986, pb.PBServerFactory(portal))
reactor.run()

Cred 插件

使用 Cred 插件进行身份验证

Cred 为身份验证方法提供插件架构。此架构的主要 API 是命令行;插件旨在由最终用户在部署 TAP(twistd 插件)时指定。

有关编写 twistd 插件和在您的应用程序中使用 cred 插件的更多信息,请参阅 编写 twistd 插件 文档。

构建 Cred 插件

要为 cred 构建插件,您应该首先定义一个 authType,这是一个简短的单字字符串,用于将您的插件定义到命令行。有了它,惯例是在 twisted.plugins 模块路径中创建一个名为 myapp_plugins.py 的文件。

以下是定义此类插件的应用程序的文件结构示例

  • MyApplication/

    • setup.py

    • myapp/

      • __init__.py

      • cred.py

      • server.py

    • twisted/

      • plugins/

        • myapp_plugins.py

在您的应用程序中创建此结构后,您可以通过构建实现 ICheckerFactory 的工厂类来创建 Cred 插件的代码。这些工厂类不应包含大量的代码。大多数真正的应用程序逻辑应该驻留在 Cred 检查器本身中。(有关构建这些检查器的帮助,请向上滚动。)

CheckerFactory 的核心目的是将命令行上传递的 argstring 转换为适合 Checker 类的初始化参数集。在大多数情况下,这应该只是构建一个字典或参数元组,然后将它们传递给新的检查器实例。

from zope.interface import implementer

from twisted import plugin
from twisted.cred.strcred import ICheckerFactory
from myapp.cred import SpecialChecker

# The class needs to implement both of these interfaces
# for the plugin system to find our factory.
@implementer(ICheckerFactory, plugin.IPlugin)
class SpecialCheckerFactory(object):
    """
    A checker factory for a specialized (fictional) API.
    """
    # This tells AuthOptionsMixin how to find this factory.
    authType = "special"

    # This is a one-line explanation of what arguments, if any,
    # your particular cred plugin requires at the command-line.
    argStringFormat = "A colon-separated key=value list."

    # This help text can be multiple lines. It will be displayed
    # when someone uses the "--help-auth-type special" command.
    authHelp = """Some help text goes here ..."""

    # This will be called once per command-line.
    def generateChecker(self, argstring=""):
        argdict = dict((x.split('=') for x in argstring.split(':')))
        return SpecialChecker(**argdict)

# We need to instantiate our class for the plugin to work.
theSpecialCheckerFactory = SpecialCheckerFactory()

有关您的插件如何在您的应用程序(以及其他应用程序开发人员)中使用的更多信息,请参阅 编写 twistd 插件 文档。

结论

阅读完本教程后,您应该能够

  • 了解 Cred 架构如何应用于您的应用程序

  • 将您的应用程序与 Cred 的对象模型集成

  • 部署使用 Cred 进行身份验证的应用程序

  • 允许您的用户使用命令行身份验证插件