HTTP 身份验证

许多之前的示例都介绍了如何使用现有的资源类或实现新的资源类来提供内容。在本例中,我们将使用 Twisted Web 的基本或摘要 HTTP 身份验证来控制对这些资源的访问。

guard 是 Twisted Web 模块,它提供了本例中将使用的大多数 API,帮助您向资源层次结构添加 身份验证授权。它通过提供一个实现 getChild 的资源来实现这一点,以返回一个 动态选择的资源。选择基于请求中的身份验证标头。如果这些标头表明请求是代表 Alice 发出的,那么将返回 Alice 的资源。如果它们表明它是代表 Bob 发出的,那么将返回他的资源。如果标头包含无效凭据,则返回错误资源。无论发生什么,一旦返回此资源,URL 遍历将从该资源开始正常进行。

实现此功能的资源是 HTTPAuthSessionWrapper,尽管它对整个过程的直接负责程度很低。它将从请求中提取标头,并将它们传递给凭据工厂,以便根据相应的标准(例如 HTTPAuthentication: Basic and Digest Access Authentication)解析它们,然后将生成的凭据对象传递给 Portal,它是 Twisted Cred 的核心,一个用于统一处理身份验证和授权的系统。我们在这里不会深入讨论 Twisted Cred。要将它与 Twisted Web 一起使用,您真正需要了解的只是如何实现一个 IRealm

您需要实现一个 realm,因为 realm 是实际决定为哪些用户使用哪些资源的对象。这可以像您的应用程序一样复杂或简单。在本例中,我们将保持非常简单:每个用户将拥有一个资源,该资源是其 UNIX 主目录中 public_html 目录的静态文件列表。首先,我们需要从 zope.interface 中导入 implements,以及从 twisted.cred.portal 中导入 IRealm。这些将共同让我将此类标记为一个 realm(这主要是一个文档问题,但并非完全如此)。我们还需要 File 用于稍后的实际实现。

from zope.interface import implementer

from twisted.cred.portal import IRealm
from twisted.web.static import File

@implementer(IRealm)
class PublicHTMLRealm(object):

一个 realm 只需要实现一个方法:requestAvatar。此方法在任何成功的身份验证尝试(即 Alice 提供了正确的密码)后被调用。它的工作是返回成功进行身份验证的用户的化身化身只是一个表示用户的对象。在本例中,它将是一个 File。通常,使用 Guard,化身必须是某种资源。

...
    def requestAvatar(self, avatarId, mind, *interfaces):
        if IResource in interfaces:
            return (IResource, File("/home/%s/public_html" % (avatarId,)), lambda: None)
        raise NotImplementedError()

关于此方法的一些说明

  • 参数 avatarId 本质上是用户名。提取用户名并确保它传递到这里,这是其他一些代码的工作。

  • 在编写要与 Guard 一起使用的 realm 时,mind 始终为 None。您可以忽略它,直到您想为其他东西编写一个 realm。

  • Guard 始终将 IResource 作为 interfaces 参数传递。如果 interfaces 只包含您的代码不理解的接口,则如上所述,引发 NotImplementedError 是要做的。只有在为除 Guard 之外的其他东西编写 realm 时,您才需要担心获取不同的接口。

  • 如果您想跟踪用户何时注销,这就是返回元组的最后一个元素的作用。当此化身注销时,它将被调用。 lambda: None 是惯用的无操作注销函数。

  • 请注意,本例中的路径处理代码编写得非常糟糕。本例可能容易受到某些非故意的信息泄露攻击。这种问题正是 FilePath 存在的原因。但是,那是另一个日子要讲的例子……

我们几乎准备好为本例设置资源了。但是,要创建一个 HTTPAuthSessionWrapper,我们需要两件事。首先,一个 portal,它需要上面的 realm,以及至少一个凭据检查器

from twisted.cred.portal import Portal
from twisted.cred.checkers import FilePasswordDB

portal = Portal(PublicHTMLRealm(), [FilePasswordDB('httpd.password')])

FilePasswordDB 是凭据检查器。它知道如何读取 passwd(5) 风格(松散地)文件以检查凭据。它负责在 HTTPAuthSessionWrapper 从请求中提取凭据后进行身份验证工作。

接下来,我们需要 BasicCredentialFactoryDigestCredentialFactory。前者知道如何挑战 HTTP 客户端进行基本身份验证;后者,摘要身份验证。我们将在这里使用摘要

from twisted.web.guard import DigestCredentialFactory

credentialFactory = DigestCredentialFactory("md5", "example.org")

此构造函数的两个参数是哈希算法和将使用的 HTTP 身份验证 realm。唯一其他有效的哈希算法是“sha”(但要小心,MD5 比 SHA 具有更广泛的支持)。HTTP 身份验证 realm 主要只是一个字符串,它被呈现给用户,让他们知道他们为什么要进行身份验证(您可以在 RFC 中阅读更多相关信息)。

创建了这些内容后,我们终于可以实例化 HTTPAuthSessionWrapper

from twisted.web.guard import HTTPAuthSessionWrapper

resource = HTTPAuthSessionWrapper(portal, [credentialFactory])

这里只剩下最后一件需要做的事情。当引入rpy 脚本时,曾提到它们在非寻常的上下文中被评估。这是第一个需要真正考虑这一点的例子。碰巧的是,DigestCredentialFactory 实例是有状态的。只有在使用同一个实例来生成挑战并检查对这些挑战的响应时,身份验证才会成功。然而,rpy 脚本的正常操作模式是,对于每个请求,它都会被重新执行。这会导致每个请求都创建一个新的 DigestCredentialFactory,从而阻止任何身份验证尝试成功。

有两种方法可以解决这个问题。首先,也是更好的方法,我们可以将几乎所有代码移到一个真正的 Python 模块中,包括实例化 DigestCredentialFactory 的代码。这将确保对每个请求都使用同一个实例。其次,也是更简单的方法,我们可以向 rpy 脚本的开头添加一个对 cache() 的调用。

cache()

cache 是任何 rpy 脚本全局变量的一部分,因此您不需要导入它(此时您可能会感到畏缩)。调用 cache 会让 Twisted 对后续请求重复使用 rpy 脚本第一次评估的结果 - 正是我们想要的结果。

以下是完整的示例(将导入重新排列为更传统的风格)

cache()

from zope.interface import implementer

from twisted.cred.portal import IRealm, Portal
from twisted.cred.checkers import FilePasswordDB
from twisted.web.static import File
from twisted.web.resource import IResource
from twisted.web.guard import HTTPAuthSessionWrapper, DigestCredentialFactory

@implementer(IRealm)
class PublicHTMLRealm(object):
    def requestAvatar(self, avatarId, mind, *interfaces):
        if IResource in interfaces:
            return (IResource, File("/home/%s/public_html" % (avatarId,)), lambda: None)
        raise NotImplementedError()

portal = Portal(PublicHTMLRealm(), [FilePasswordDB('httpd.password')])

credentialFactory = DigestCredentialFactory("md5", "localhost:8080")
resource = HTTPAuthSessionWrapper(portal, [credentialFactory])

瞧,一个受密码保护的每个用户 Twisted Web 服务器。