使用 Perspective Broker 进行身份验证

概述

使用 Perspective Broker 中展示的示例演示了如何进行基本的远程方法调用,但没有提供身份验证的设施。在这种情况下,身份验证是指谁获得哪些远程引用,以及如何限制对“正确”人员或程序的访问。

一旦你拥有一个向多个用户提供服务的程序,而这些用户不应该相互干扰,你就需要考虑身份验证。许多服务使用“帐户”的概念,并依赖于每个用户只能访问一个帐户这一事实。Twisted 使用一个名为 cred 的系统来处理身份验证问题,Perspective Broker 拥有代码,可以轻松实现最常见的用例。

对服务进行分隔

想象一下,你将如何使用 PB 编写一个聊天服务器。第一步可能是创建一个 ChatServer 对象,该对象包含一组指向用户客户端的 pb.RemoteReference。假设这些客户端提供了一个 remote_print 方法,该方法允许服务器在用户的控制台上打印一条消息。在这种情况下,服务器可能看起来像这样

class ChatServer(pb.Referenceable):

    def __init__(self):
        self.groups = {} # indexed by name
        self.users = {} # indexed by name
    def remote_joinGroup(self, username, groupname):
        if groupname not in self.groups:
            self.groups[groupname] = []
        self.groups[groupname].append(self.users[username])
    def remote_sendMessage(self, from_username, groupname, message):
        group = self.groups[groupname]
        if group:
            # send the message to all members of the group
            for user in group:
                user.callRemote("print",
                                "<%s> says: %s" % (from_username,
                                                         message))

现在,假设所有客户端都以某种方式获得了指向此 ChatServer 对象的 pb.RemoteReference,也许是使用 pb.RootgetRootObject,如 上一章 所述。在这种方案中,当用户向组发送消息时,他们的客户端会运行类似以下内容

remotegroup.callRemote("sendMessage", "alice", "Hi, my name is alice.")

错误的参数

你可能已经看到了第一个问题:用户可以轻松地伪造彼此。我们依赖于用户在他们的“username”参数中传递正确的值,并且没有办法判断他们是否在撒谎。没有什么可以阻止 Alice 修改她的客户端来执行以下操作

remotegroup.callRemote("sendMessage", "bob", "i like pork")

让 Bob 的素食朋友感到恐惧。 [1]

(一般来说,如果你看到任何远程可调用方法的参数被描述为“必须是 X”,那么要学会怀疑。)

解决此问题的最佳方法是本地跟踪用户的姓名,而不是要求他们在每次发送消息时将其发送到服务器。保存状态的最佳位置是在对象中,因此这表明我们需要一个针对每个用户的对象。与其选择一个明显的名字 [2],不如将其称为 User 类。

class User(pb.Referenceable):
    def __init__(self, username, server, clientref):
        self.name = username
        self.server = server
        self.remote = clientref
    def remote_joinGroup(self, groupname):
        self.server.joinGroup(groupname, self)
    def remote_sendMessage(self, groupname, message):
        self.server.sendMessage(self.name, groupname, message)
    def send(self, message):
        self.remote.callRemote("print", message)

class ChatServer:
    def __init__(self):
        self.groups = {} # indexed by name
    def joinGroup(self, groupname, user):
        if groupname not in self.groups:
            self.groups[groupname] = []
        self.groups[groupname].append(user)
    def sendMessage(self, from_username, groupname, message):
        group = self.groups[groupname]
        if group:
            # send the message to all members of the group
            for user in group:
                user.send("<%s> says: %s" % (from_username, message))

同样,假设每个远程客户端都获得对单个 User 对象的访问权限,该对象使用正确的用户名创建。

请注意,ChatServer 对象没有远程访问权限:它甚至不再是 pb.Referenceable。这意味着对它的所有访问都必须通过其他对象进行中介,这些对象的代码在你的控制之下。

只要 Alice 只能访问她自己的 User 对象,她就无法再伪造 Bob。她调用 ChatServer.sendMessage 的唯一方法是调用她 User 对象的 remote_sendMessage 方法,而该方法使用它自己的状态来提供 from_username 参数。它不会给她任何改变该状态的方法。

这种限制很重要。 User 对象能够保持自身的完整性,因为在对象和客户端之间存在一个屏障:客户端无法检查或修改内部状态,例如 .name 属性。穿过此屏障的唯一方法是通过远程方法调用,而 Alice 对这些调用的唯一控制权是它们何时被调用以及传递了哪些参数。

注意

没有对象能够抵御本地威胁:根据设计,Python 没有提供任何机制让类实例隐藏它们的属性,一旦入侵者获得了 self.__dict__ 的副本,他们就可以执行原始对象能够执行的所有操作。

不可伪造的引用

现在假设你想要实现组参数,例如,没有人被允许谈论床垫的模式,因为一些用户很敏感,在有人说“床垫”后安抚他们是一件很麻烦的事情,最好避免 altogether。同样,每个组的状态都意味着每个组都有一个对象。我们将冒险将其称为 Group 对象

class User(pb.Referenceable):
    def __init__(self, username, server, clientref):
        self.name = username
        self.server = server
        self.remote = clientref
    def remote_joinGroup(self, groupname, allowMattress=True):
        return self.server.joinGroup(groupname, self, allowMattress)
    def send(self, message):
        self.remote.callRemote("print", message)

class Group(pb.Referenceable):
    def __init__(self, groupname, allowMattress):
        self.name = groupname
        self.allowMattress = allowMattress
        self.users = []
    def remote_send(self, from_user, message):
        if not self.allowMattress and "mattress" in message:
            raise ValueError("Don't say that word")
        for user in self.users:
            user.send("<%s> says: %s" % (from_user.name, message))
    def addUser(self, user):
        self.users.append(user)

class ChatServer:
    def __init__(self):
        self.groups = {} # indexed by name
    def joinGroup(self, groupname, user, allowMattress):
        if groupname not in self.groups:
            self.groups[groupname] = Group(groupname, allowMattress)
        self.groups[groupname].addUser(user)
        return self.groups[groupname]

此示例利用了这样一个事实:通过网络发送的 pb.Referenceable 对象可以返回给你,它们将被转换为指向你最初发送的同一个对象的引用。客户端无法以任何方式修改该对象:他们所能做的就是指向它并调用它的 remote_* 方法。因此,你可以确保 .name 属性保持与你离开时相同。在这种情况下,客户端代码可能看起来像这样

class ClientThing(pb.Referenceable):
    def remote_print(self, message):
        print(message)
    def join(self):
        d = self.remoteUser.callRemote("joinGroup", "#twisted",
                                       allowMattress=False)
        d.addCallback(self.gotGroup)
    def gotGroup(self, group):
        group.callRemote("send", self.remoteUser, "hi everybody")

来自服务器端的 User 对象被发送到客户端,并在到达客户端时被转换为 pb.RemoteReference。客户端将其发送回 Group.remote_send,PB 在到达那里时将其转换回对原始 User 的引用。然后,Group.remote_send 可以使用其 .name 属性作为消息的发送者。

注意

第三方引用(没有)

此技术还依赖于这样一个事实,即 pb.Referenceable 引用只能来自持有相应 pb.RemoteReference 的人。序列化机制的设计(在 twisted.spread.jelly 中实现:pb、jelly、spread.. 懂了吗?也找找“banana”。还有哪个网络框架可以声称其 API 名称是基于三明治配料的?)使得客户端不可能获得他们没有明确获得的引用。通过网络传递的引用被赋予 ID 号并记录在每个连接的字典中。如果你没有给他们引用,ID 号就不会在字典中,恶意客户端无论如何猜测都无法获得其他任何东西。当连接断开时,字典会消失,进一步限制了这些引用的范围。

此外,Bob 无法将他的 User 引用发送给 Alice(也许通过他们之间的一些其他 PB 通道)。在 Bob 与服务器连接的上下文之外,该引用只是一个毫无意义的数字。为了防止混淆,PB 会在您尝试将其赠送时告诉您:当您尝试将 pb.RemoteReference 赠送给第三方时,您将收到一个异常(在 pb.py:364 RemoteReference.jellyFor 中使用断言实现)。

这在一定程度上帮助了安全模型:只有您授予引用的客户端才能用它造成任何损害。当然,客户端可能是一个没有脑子的僵尸,只是简单地按照第三方的意愿行事。当它没有代理 callRemote 调用时,它可能正在恐吓活人并寻找人类大脑作为食物。简而言之,如果您不信任他们,就不要给他们那个引用。

请记住,您曾经通过该连接发送给他们的所有内容都可能返回给您。如果您期望客户端使用您之前发送给他们的某个对象 A 来调用您的方法,而他们却发送给您对象 B(您之前也发送给了他们),并且您没有以某种方式进行检查,那么您就打开了安全漏洞(我们很快就会看到一个例子)。最好将此类对象保存在服务器端的字典中,让客户端发送一个索引字符串。这样做可以清楚地表明他们可以发送任何他们想要的东西,并提高您记住实现正确检查的可能性。(这正是 PB 在底层使用每个连接的 Referenceable 对象字典,并通过数字进行索引的方式)。

当然,您必须确保不会意外地将引用传递给错误的对象。

但同样,请注意漏洞。如果 Alice 持有对服务器端任何具有 .name 属性的对象的 RemoteReference,她可以使用该名称作为“来自”参数的“伪造”。举个简单的例子,如果她的客户端代码如下所示

class ClientThing(pb.Referenceable):
    def join(self):
        d = self.remoteUser.callRemote("joinGroup", "#twisted")
        d.addCallback(self.gotGroup)
    def gotGroup(self, group):
        group.callRemote("send", from_user=group, "hi everybody")

这将允许她发送一条看起来来自“#twisted”而不是“Alice”的消息。如果她加入了一个恰好名为“bob”的组(也许是“如何成为 Bob”频道,由 Alice 和无数其他人组成,他们可以在那里分享他们最棒的模仿 Bob 的时刻的故事),那么她将能够发出看起来像“<bob> 说:你好”的消息,她已经实现了她的毕生目标。

参数类型检查

有两种技术可以关闭这个漏洞。第一种是让您可远程调用的方法对其参数进行类型检查:如果 Group.remote_send 断言 isinstance(from_user, User),那么 Alice 就无法使用非 User 对象进行伪造,并且希望系统的其余部分设计得足够好,以防止她获得对其他人 User 对象的访问权限。

对象作为能力

第二种技术是避免让客户端完全发送对象。如果他们不发送任何东西,就没有什么需要验证。在这种情况下,您需要有一个每个用户每个组的对象,其中 remote_send 方法只接受一个 message 参数。UserGroup 对象是用对它将永远使用的唯一 UserGroup 对象的引用创建的,因此不需要进行查找

class UserGroup(pb.Referenceable):
    def __init__(self, user, group):
        self.user = user
        self.group = group
    def remote_send(self, message):
        self.group.send(self.user.name, message)

class Group:
    def __init__(self, groupname, allowMattress):
        self.name = groupname
        self.allowMattress = allowMattress
        self.users = []
    def send(self, from_user, message):
        if not self.allowMattress and "mattress" in message:
            raise ValueError("Don't say that word")
        for user in self.users:
            user.send("<%s> says: %s" % (from_user.name, message))
    def addUser(self, user):
        self.users.append(user)

Alice 剩下的唯一消息发送方法是 UserGroup.remote_send,它只接受一条消息:没有其他方法可以影响“来自”名称。

在此模型中,每个可远程访问的对象代表一组非常小的功能。通过仅向每个远程用户授予最小的一组功能来实现安全性。

PB 提供了一个快捷方式,使这种技术更容易使用。Viewable 类将在 下面 讨论。

化身和视角

在 Twisted 的 cred 系统中,“化身”是一个存在于“服务器”端(这里定义为距离试图完成某事的用户最远的一端)的对象,它允许远程用户完成某事。化身实际上不是一个特定的类,更像是一种描述某个对象扮演的角色,例如“这里的 Foo 对象充当用户在此特定服务的化身”。通常,远程用户有一些方法可以让他们的化身运行一些代码。化身对象可能会执行一些安全检查并提供其他数据,然后调用其他方法来完成事情。

cred 拼图中的两块(对于任何协议,不仅仅是 PB)是:“什么充当化身?”,以及“用户如何访问它?”。

对于 PB,第一个问题很容易。化身是一个可远程访问的对象,可以运行代码:这是对 pb.Referenceable 及其子类的完美描述。我们将把第二个问题留到下一节。

在上面的示例中,您可以将 ChatServerGroup 对象视为一项服务。User 对象是用户的服务器端代表:用户能够执行的所有操作都是通过运行其方法之一来完成的。服务器想要对用户执行的任何操作(更改其组成员资格、更改其名称、删除其宠物猫,等等)都是通过操作 User 对象来完成的。

在 ChatServer 周围和平共处着多个 User 对象。每个对象对 ChatServer 和 Group 提供的服务都有不同的观点:每个对象可能属于不同的组,有些对象可能比其他对象具有更多权限(例如创建组的能力)。这些不同的观点被称为“视角”。这是“视角代理”中“视角”一词的起源:PB 提供并控制(即“代理”)对视角的访问。

曾经,这些本地代表对象实际上被称为 pb.Perspective。但这随着重写的 cred 系统的出现而改变,现在对本地代表对象的更通用术语是化身。但是您仍然会在代码、文档和模块名称中看到对“视角”的引用 [3]。请记住,视角和化身基本上是同一件事。

尽管我们一直在 告诉您 化身更像是一个概念而不是一个实际的类,但您可以用来创建服务器端化身式对象的基类实际上名为 pb.Avatar [4]。这些对象的行为非常类似于 pb.Referenceable。唯一的区别是,它们不是提供“remote_FOO”方法,而是提供“perspective_FOO”方法。

pb.Referenceable 不同的另一个方面是,化身对象被设计为由使用 cred 的远程客户端检索的第一个对象。就像 PBClientFactory.getRootObject 为客户端提供对 pb.Root 对象的访问权限(然后可以提供对各种其他对象的访问权限)一样,PBClientFactory.login 为客户端提供对 pb.Avatar 对象的访问权限(可以返回其他引用)。

因此,在 PB 应用程序中使用 cred 的第一步是创建一个 Avatar 对象,它实现 perspective_ 方法,并小心地为远程用户执行有用的操作,同时警惕被意外的参数值欺骗。它还必须小心,永远不要访问用户不应该访问的对象,无论是直接返回它们、返回包含它们的 对象,还是返回可以被(远程)询问以提供它们的 对象。

第二部分介绍用户如何获取指向您 Avatar 的 pb.RemoteReference。如 其他地方 所述,Avatar 来自 Realm。Realm 根本不处理身份验证(用户名、密码、公钥、质询-响应系统、视网膜扫描仪、实时 DNA 测序仪等)。它只接受一个“avatarID”(实际上是一个用户名)并返回一个 Avatar 对象。Portal 及其 Checker 处理用户的身份验证:在它们完成之前,远程用户已证明他们有权访问提供给 Realm 的 avatarID,因此 Realm 可以返回一个可远程控制的对象,该对象具有您希望授予此特定用户的任何权限。

对于 PB,Realm 预计会返回一个 pb.Avatar(或任何实现 pb.IPerspective 的对象,但没有理由不返回 pb.Avatar 子类)。此对象将像 pb.Root 一样提供给客户端,而无需凭据,用户可以通过它访问其他对象(如果您允许他们)。

基本思想是,每个用户都有一个单独的实现 IPerspective 的对象(即 Avatar 子类)(即“视角”),并且*只有*授权用户才能获得对该对象的远程引用。您可以在该对象中存储用户拥有的任何权限或功能,然后在用户调用远程方法时使用它们。您向用户提供对视角对象的访问权限,而不是对执行实际工作的对象的访问权限。

视角示例

以下是一个使用 pb.Avatar 的简短示例。大多数支持代码目前都是魔法:我们将在后面解释它。

一个客户端

pb5server.py

#!/usr/bin/env python

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


from zope.interface import implementer

from twisted.cred import checkers, portal
from twisted.internet import reactor
from twisted.spread import pb


class MyPerspective(pb.Avatar):
    def __init__(self, name):
        self.name = name

    def perspective_foo(self, arg):
        print("I am", self.name, "perspective_foo(", arg, ") called on", self)


@implementer(portal.IRealm)
class MyRealm:
    def requestAvatar(self, avatarId, mind, *interfaces):
        if pb.IPerspective not in interfaces:
            raise NotImplementedError
        return pb.IPerspective, MyPerspective(avatarId), lambda: None


p = portal.Portal(MyRealm())
p.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(user1="pass1"))
reactor.listenTCP(8800, pb.PBServerFactory(p))
reactor.run()

pb5client.py

#!/usr/bin/env python

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


from twisted.cred import credentials
from twisted.internet import reactor
from twisted.spread import pb


def main():
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    def1 = factory.login(credentials.UsernamePassword("user1", "pass1"))
    def1.addCallback(connected)
    reactor.run()


def connected(perspective):
    print("got perspective ref:", perspective)
    print("asking it to foo(12)")
    perspective.callRemote("foo", 12)


main()

好的,所以这并不真正令人兴奋。它并没有比第一个 PB 示例完成更多的事情,并且使用了更多代码来实现它。让我们这次尝试使用两个用户。

注意

当客户端运行 login 以请求视角时,它们可以为其提供一个可选的 client 参数(它必须是一个 pb.Referenceable 对象)。如果他们这样做,那么对该对象的引用将被传递给 Realm 的 requestAvatar 中的 mind 参数。

服务器端的视角可以使用它来调用客户端中某个对象的远程方法,这样客户端就不必总是驱动交互。在聊天服务器中,客户端对象将是发送“显示文本”消息的对象。在棋盘游戏服务器中,这将提供一种方法来告诉客户端有人已经进行了移动,以便他们可以更新自己的游戏棋盘。

两个客户端

pb6server.py

#!/usr/bin/env python

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


from zope.interface import implementer

from twisted.cred import checkers, portal
from twisted.internet import reactor
from twisted.spread import pb


class MyPerspective(pb.Avatar):
    def __init__(self, name):
        self.name = name

    def perspective_foo(self, arg):
        print("I am", self.name, "perspective_foo(", arg, ") called on", self)


@implementer(portal.IRealm)
class MyRealm:
    def requestAvatar(self, avatarId, mind, *interfaces):
        if pb.IPerspective not in interfaces:
            raise NotImplementedError
        return pb.IPerspective, MyPerspective(avatarId), lambda: None


p = portal.Portal(MyRealm())
c = checkers.InMemoryUsernamePasswordDatabaseDontUse(user1="pass1", user2="pass2")
p.registerChecker(c)
reactor.listenTCP(8800, pb.PBServerFactory(p))
reactor.run()

pb6client1.py

#!/usr/bin/env python

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


from twisted.cred import credentials
from twisted.internet import reactor
from twisted.spread import pb


def main():
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    def1 = factory.login(credentials.UsernamePassword("user1", "pass1"))
    def1.addCallback(connected)
    reactor.run()


def connected(perspective):
    print("got perspective1 ref:", perspective)
    print("asking it to foo(13)")
    perspective.callRemote("foo", 13)


main()

pb6client2.py

#!/usr/bin/env python

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


from twisted.cred import credentials
from twisted.internet import reactor
from twisted.spread import pb


def main():
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    def1 = factory.login(credentials.UsernamePassword("user2", "pass2"))
    def1.addCallback(connected)
    reactor.run()


def connected(perspective):
    print("got perspective2 ref:", perspective)
    print("asking it to foo(14)")
    perspective.callRemote("foo", 14)


main()

当 pb6server.py 运行时,尝试先启动 pb6client1,然后启动 pb6client2。比较每个客户端中 .callRemote() 传递的参数。您可以看到每个客户端如何连接到不同的视角。

该示例是如何工作的

让我们逐步了解前面的示例,看看发生了什么。

首先,我们创建了一个名为 MyPerspective 的子类,它是我们的服务器端 Avatar。它实现了一个 perspective_foo 方法,该方法公开给远程客户端。

其次,我们创建了一个 Realm(一个实现 IRealm 的对象,因此也实现 requestAvatar)。此 Realm 制造 MyPerspective 对象。它可以制造我们想要的任何数量,并使用从 Checker 中获得的 avatarID(用户名)为每个对象命名。此 MyRealm 对象还返回另外两个对象,我们将在后面描述它们。

第三,我们创建了一个 Portal 来容纳此 Realm。Portal 的工作是将传入的客户端分派给凭据 Checker,然后为通过身份验证过程的任何客户端请求 Avatar。

第四,我们创建了一个简单的 Checker(一个实现 IChecker 的对象)来保存有效的用户/密码对。Checker 注册到 Portal,以便它知道在新的客户端连接时要询问谁。我们使用了一个名为 InMemoryUsernamePasswordDatabaseDontUse 的 Checker,这表明:1. 所有用户名/密码对都保存在内存中,而不是保存到数据库或其他地方,2. 您不应该使用它。反对使用它的告诫是因为存在更好的方案:当您需要跟踪数千或数百万用户时,将所有内容保存在内存中将不起作用,密码将在应用程序关闭时存储在 .tap 文件中(可能存在安全风险),最后,在 Checker 构造之后添加或删除用户会很麻烦。

第五,我们创建一个 pb.PBServerFactory 来监听 TCP 端口。此工厂知道如何将远程客户端连接到 Portal,因此传入的连接将被传递给身份验证过程。其他协议(非 PB)也会执行类似的操作:创建 Protocol 对象的工厂将为这些对象提供对 Portal 的访问权限,以便进行身份验证。

在客户端,创建一个 pb.PBClientFactory(如 之前 所述)并将其附加到 TCP 连接。当连接完成时,将要求工厂生成一个 Protocol,它将创建一个 PB 对象。与上一章不同,我们使用 .getRootObject,在这里我们使用 factory.login 来启动凭据身份验证过程。我们提供一个 credentials 对象,它是用于执行我们一半身份验证过程的客户端代理。此过程可能涉及多个消息:质询、响应、加密密码、安全哈希等。我们为我们的凭据对象提供它需要正确响应的所有内容(在本例中,用户名和密码,但您可以编写一个使用公钥加密甚至更高级技术的凭据)。

login 返回一个 Deferred,当它触发时,将返回一个指向远程 Avatar 的 pb.RemoteReference。然后,我们可以执行 callRemote 来调用该 Avatar 上的 perspective_foo 方法。

匿名客户端

pbAnonServer.py

#!/usr/bin/env python

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

"""
Implement the realm for and run on port 8800 a PB service which allows both
anonymous and username/password based access.

Successful username/password-based login requests given an instance of
MyPerspective with a name which matches the username with which they
authenticated.  Success anonymous login requests are given an instance of
MyPerspective with the name "Anonymous".
"""


from sys import stdout

from zope.interface import implementer

from twisted.cred.checkers import (
    ANONYMOUS,
    AllowAnonymousAccess,
    InMemoryUsernamePasswordDatabaseDontUse,
)
from twisted.cred.portal import IRealm, Portal
from twisted.internet import reactor
from twisted.python.log import startLogging
from twisted.spread.pb import Avatar, IPerspective, PBServerFactory


class MyPerspective(Avatar):
    """
    Trivial avatar exposing a single remote method for demonstrative
    purposes.  All successful login attempts in this example will result in
    an avatar which is an instance of this class.

    @type name: C{str}
    @ivar name: The username which was used during login or C{"Anonymous"}
    if the login was anonymous (a real service might want to avoid the
    collision this introduces between anonoymous users and authenticated
    users named "Anonymous").
    """

    def __init__(self, name):
        self.name = name

    def perspective_foo(self, arg):
        """
        Print a simple message which gives the argument this method was
        called with and this avatar's name.
        """
        print(f"I am {self.name}.  perspective_foo({arg}) called on {self}.")


@implementer(IRealm)
class MyRealm:
    """
    Trivial realm which supports anonymous and named users by creating
    avatars which are instances of MyPerspective for either.
    """

    def requestAvatar(self, avatarId, mind, *interfaces):
        if IPerspective not in interfaces:
            raise NotImplementedError("MyRealm only handles IPerspective")
        if avatarId is ANONYMOUS:
            avatarId = "Anonymous"
        return IPerspective, MyPerspective(avatarId), lambda: None


def main():
    """
    Create a PB server using MyRealm and run it on port 8800.
    """
    startLogging(stdout)

    p = Portal(MyRealm())

    # Here the username/password checker is registered.
    c1 = InMemoryUsernamePasswordDatabaseDontUse(user1="pass1", user2="pass2")
    p.registerChecker(c1)

    # Here the anonymous checker is registered.
    c2 = AllowAnonymousAccess()
    p.registerChecker(c2)

    reactor.listenTCP(8800, PBServerFactory(p))
    reactor.run()


if __name__ == "__main__":
    main()

pbAnonClient.py

#!/usr/bin/env python

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

"""
Client which will talk to the server run by pbAnonServer.py, logging in
either anonymously or with username/password credentials.
"""


from sys import stdout

from twisted.cred.credentials import Anonymous, UsernamePassword
from twisted.internet import reactor
from twisted.internet.defer import gatherResults
from twisted.python.log import err, startLogging
from twisted.spread.pb import PBClientFactory


def error(why, msg):
    """
    Catch-all errback which simply logs the failure.  This isn't expected to
    be invoked in the normal case for this example.
    """
    err(why, msg)


def connected(perspective):
    """
    Login callback which invokes the remote "foo" method on the perspective
    which the server returned.
    """
    print("got perspective1 ref:", perspective)
    print("asking it to foo(13)")
    return perspective.callRemote("foo", 13)


def finished(ignored):
    """
    Callback invoked when both logins and method calls have finished to shut
    down the reactor so the example exits.
    """
    reactor.stop()


def main():
    """
    Connect to a PB server running on port 8800 on localhost and log in to
    it, both anonymously and using a username/password it will recognize.
    """
    startLogging(stdout)
    factory = PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)

    anonymousLogin = factory.login(Anonymous())
    anonymousLogin.addCallback(connected)
    anonymousLogin.addErrback(error, "Anonymous login failed")

    usernameLogin = factory.login(UsernamePassword("user1", "pass1"))
    usernameLogin.addCallback(connected)
    usernameLogin.addErrback(error, "Username/password login failed")

    bothDeferreds = gatherResults([anonymousLogin, usernameLogin])
    bothDeferreds.addCallback(finished)

    reactor.run()


if __name__ == "__main__":
    main()

pbAnonServer.py 实现了一个基于 pb6server.py 的服务器,扩展它以允许匿名登录,除了经过身份验证的登录。一个 AllowAnonymousAccess Checker 和一个 InMemoryUsernamePasswordDatabaseDontUse Checker 被注册,客户端选择的凭据对象决定使用哪个 Checker 来验证登录。无论哪种情况,都会调用 Realm 来为登录创建 Avatar。 AllowAnonymousAccess 始终生成一个 avatarId,值为 twisted.cred.checkers.ANONYMOUS

在客户端,唯一的变化是在调用 PBClientFactory.login 时使用 Anonymous 的实例。

使用 Avatar

Avatar 接口

requestAvatar 返回的 3 元组的第一个元素指示此 Avatar 实现哪个接口。对于 PB Avatar,它将始终是 pb.IPerspective,因为这是这些 Avatar 实现的唯一接口。

此元素存在是因为 requestAvatar 实际上提供了一个可能的接口列表。向 Realm 提出的问题是:“您是否拥有一个可以实现以下接口集之一的(avatarID)的 Avatar?”。一些 Portal 和 Checker 可能会提供一个接口列表,Realm 可以进行选择;PB 代码只知道如何执行一个操作,因此我们无法利用此功能。

注销

3 元组的第三个元素是一个不带参数的可调用对象,它将在连接丢失时由协议调用。我们可以使用它在客户端丢失连接时通知 Avatar。这将在下面详细描述。

创建 Avatar

在上面的示例中,我们在 requestAvatar 期间根据请求创建 Avatar。根据服务,这些 Avatar 可能会在收到连接之前就已经存在,并且可能会比连接存活更长时间。Avatar 也可能接受多个连接。

另一种可能性是,Avatar 可能会提前存在,但以不同的形式(冻结在 pickle 中和/或保存在数据库中)。在这种情况下,requestAvatar 可能需要执行数据库查找,然后对结果执行某些操作,然后才能提供 Avatar。在这种情况下,它可能会返回一个 Deferred,以便它可以在查找完成后提供真正的 Avatar。

以下是 MyRealm.requestAvatar 的一些可能的实现

# pre-existing, static avatars
def requestAvatar(self, avatarID, mind, *interfaces):
    assert pb.IPerspective in interfaces
    avatar = self.avatars[avatarID]
    return pb.IPerspective, avatar, lambda:None

# database lookup and unpickling
def requestAvatar(self, avatarID, mind, *interfaces):
    assert pb.IPerspective in interfaces
    d = self.database.fetchAvatar(avatarID)
    d.addCallback(self.doUnpickle)
    return pb.IPerspective, d, lambda:None
def doUnpickle(self, pickled):
    avatar = pickle.loads(pickled)
    return avatar

# everybody shares the same Avatar
def requestAvatar(self, avatarID, mind, *interfaces):
    assert pb.IPerspective in interfaces
    return pb.IPerspective, self.theOneAvatar, lambda:None

# anonymous users share one Avatar, named users each get their own
def requestAvatar(self, avatarID, mind, *interfaces):
    assert pb.IPerspective in interfaces
    if avatarID == checkers.ANONYMOUS:
        return pb.IPerspective, self.anonAvatar, lambda:None
    else:
        return pb.IPerspective, self.avatars[avatarID], lambda:None

# anonymous users get independent (but temporary) Avatars
# named users get their own persistent one
def requestAvatar(self, avatarID, mind, *interfaces):
    assert pb.IPerspective in interfaces
    if avatarID == checkers.ANONYMOUS:
        return pb.IPerspective, MyAvatar(), lambda:None
    else:
        return pb.IPerspective, self.avatars[avatarID], lambda:None

在最后一个例子中,请注意新的 MyAvatar 实例没有保存到任何地方:当连接断开时,它将消失。相比之下,存在于 self.avatars 字典中的化身可能会与 Realm、Portal 和应用程序对象顶层引用的任何其他内容一起持久化到 .tap 文件中。这是一种管理保存的用户配置文件的简单方法。

连接和断开连接

当远程客户端获得(和失去)对它们的访问权限时,通知您的化身可能很有用。例如,化身可能会被服务器中的某些内容更新,如果存在连接的客户端,它应该更新它们(通过“mind”参数,该参数允许化身在客户端上执行 callRemote)。

一种实现此目的的常见习惯用法是让 Realm 告诉化身远程客户端刚刚连接。Realm 还可以要求协议在连接断开时通知它,以便它可以随后通知化身客户端已断开连接。 requestAvatar 返回元组的第三个成员是一个可调用对象,当连接丢失时将调用它。

class MyPerspective(pb.Avatar):
    def __init__(self):
        self.clients = []
    def attached(self, mind):
        self.clients.append(mind)
        print("attached to", mind)
    def detached(self, mind):
        self.clients.remove(mind)
        print("detached from", mind)
    def update(self, message):
        for c in self.clients:
            c.callRemote("update", message)

class MyRealm:
    def requestAvatar(self, avatarID, mind, *interfaces):
        assert pb.IPerspective in interfaces
        avatar = self.avatars[avatarID]
        avatar.attached(mind)
        return pb.IPerspective, avatar, lambda a=avatar:a.detached(mind)

可视化

一旦您拥有 IPerspective 对象(即化身)来表示用户, Viewable 类就可以发挥作用。此类在行为上与 Referenceable 非常相似:它在通过网络发送时会变成 RemoteReference,并且该引用的持有者可以调用某些方法。但是,可以调用的方法的名称以 view_ 开头,而不是 remote_,并且这些方法始终使用额外的 perspective 参数调用,该参数指向通过该引用发送引用的化身。

class Foo(pb.Viewable):
    def view_doFoo(self, perspective, arg1, arg2):
        pass

如果您想让多个客户端共享对同一对象的引用,这将很有用。 view_ 方法可以使用“perspective”参数来确定哪个客户端正在调用它们。这使它们能够执行额外的权限检查、执行每个用户的计费等。

这是使每个用户每个组的功能对象更容易使用的快捷方式。您无需创建此类每个(用户,组)对象,只需创建继承自 pb.Viewable 的每个组对象,并向用户提供对它们的引用。本地 pb.Avatar 对象将自动显示为 view_* 方法调用中的“perspective”参数,让您有机会让化身参与该过程。

带有化身的聊天服务器

结合以上所有技术,这里是一个使用固定身份集的示例聊天服务器(例如,对于您桥牌俱乐部的三个成员,他们在“#NeedAFourth”中闲逛,希望有人能发现您的服务器,猜出某人的密码,闯入,加入该组,并且也能够参加下周六下午的比赛)。

chatserver.py

#!/usr/bin/env python

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

from zope.interface import implementer

from twisted.cred import checkers, portal
from twisted.internet import reactor
from twisted.spread import pb


class ChatServer:
    def __init__(self):
        self.groups = {}  # indexed by name

    def joinGroup(self, groupname, user, allowMattress):
        if groupname not in self.groups:
            self.groups[groupname] = Group(groupname, allowMattress)
        self.groups[groupname].addUser(user)
        return self.groups[groupname]


@implementer(portal.IRealm)
class ChatRealm:
    def requestAvatar(self, avatarID, mind, *interfaces):
        assert pb.IPerspective in interfaces
        avatar = User(avatarID)
        avatar.server = self.server
        avatar.attached(mind)
        return pb.IPerspective, avatar, lambda a=avatar: a.detached(mind)


class User(pb.Avatar):
    def __init__(self, name):
        self.name = name

    def attached(self, mind):
        self.remote = mind

    def detached(self, mind):
        self.remote = None

    def perspective_joinGroup(self, groupname, allowMattress=True):
        return self.server.joinGroup(groupname, self, allowMattress)

    def send(self, message):
        self.remote.callRemote("print", message)


class Group(pb.Viewable):
    def __init__(self, groupname, allowMattress):
        self.name = groupname
        self.allowMattress = allowMattress
        self.users = []

    def addUser(self, user):
        self.users.append(user)

    def view_send(self, from_user, message):
        if not self.allowMattress and "mattress" in message:
            raise ValueError("Don't say that word")
        for user in self.users:
            user.send(f"<{from_user.name}> says: {message}")


realm = ChatRealm()
realm.server = ChatServer()
checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
checker.addUser("alice", "1234")
checker.addUser("bob", "secret")
checker.addUser("carol", "fido")
p = portal.Portal(realm, [checker])

reactor.listenTCP(8800, pb.PBServerFactory(p))
reactor.run()

请注意,客户端使用 perspective_joinGroup 来加入组并检索对 Group 对象的 RemoteReference。但是,他们获得的引用实际上是指向一个称为 pb.ViewPoint 的特殊中间对象。当他们执行 group.callRemote("send", "message") 时,他们的化身将被插入到 Group.view_send 实际看到的参数列表中。这使组能够从化身中获取其用户名,而不会给客户端提供冒充他人的机会。

加入组并发送消息的客户端代码如下所示

chatclient.py

#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


from twisted.cred import credentials
from twisted.internet import reactor
from twisted.spread import pb


class Client(pb.Referenceable):
    def remote_print(self, message):
        print(message)

    def connect(self):
        factory = pb.PBClientFactory()
        reactor.connectTCP("localhost", 8800, factory)
        def1 = factory.login(credentials.UsernamePassword("alice", "1234"), client=self)
        def1.addCallback(self.connected)
        reactor.run()

    def connected(self, perspective):
        print("connected, joining group #NeedAFourth")
        # this perspective is a reference to our User object.  Save a reference
        # to it here, otherwise it will get garbage collected after this call,
        # and the server will think we logged out.
        self.perspective = perspective
        d = perspective.callRemote("joinGroup", "#NeedAFourth")
        d.addCallback(self.gotGroup)

    def gotGroup(self, group):
        print("joined group, now sending a message to all members")
        # 'group' is a reference to the Group object (through a ViewPoint)
        d = group.callRemote("send", "You can call me Al.")
        d.addCallback(self.shutdown)

    def shutdown(self, result):
        reactor.stop()


Client().connect()

脚注