【问题标题】:PubNub security groups managementPubNub 安全组管理
【发布时间】:2016-08-05 09:31:50
【问题描述】:

我们正在构建一个应用程序,我们决定使用 pubnub 作为通知和聊天的传输方式。 这是要求

  • 用户应该能够在他们之间直接通信,
  • 只有两个正在讨论的用户才能看到对话
  • 我假设发布和订阅密钥都被泄露了
  • 服务器应该能够为每个用户发送通知
  • 用户应该只能看到他的通知

** 在某些时候,我意识到您不能使用两组密钥发布到同一个频道 - 因为一组密钥标识了频道 - 所以服务器主密钥集和客户端无级别密钥不是一个选项**

因此,最重要的是,通过使用 pam,这是通知流程

使用密钥的服务器在全局级别撤销 pub/sub 密钥的任何权限 {读:假,写:假,管理:假} 这样,没有人可以在全局级别上使用键做任何事情

用户发送一个令牌 - 此令牌成为 authKey - 只有使用该身份验证密钥,用户才能在通知通道上只读(称为“notif-{userId}”)

现在服务器需要有某种 ma​​sterKey 可以发布到所有频道 - 所以合乎逻辑的做法是发出带有 { read: true, 的 .grant() 请求写:真,管理:真,authKey:MASTER_KEY} 这里我们失败了 - 因为它响应“仅授权授权被保留以供将来使用”

现在对于聊天,想法是创建一个频道名称“chat-{userId1}-{userId2}”并将此频道添加到组“chats-{userId1}”和“chats-{userId2}”和而不是 .grant() 基于令牌对每个用户频道的权限 - 授权成功但用户订阅频道组 - 但实际发布到组失败并出现 状态 : { 类别:“PNAccessDeniedCategory”, 操作:“PNPublishOperation” }

这里是复制问题的代码示例

    'use strict';

const pubnubConf = require('../config/pubnub-master');

const PubNub = require('pubnub');
const masterKeys = pubnubConf.getMasterKeys();
const pubnub = new PubNub(masterKeys);
const pubnubSecret = new PubNub(pubnubConf.getSecretKeys());

const token = 'lklkdjwdq';

masterKeys.authKey = token;
const pubnubClient = new PubNub(masterKeys);
const userAGroup = 'chats-aaa';
const userBGroup = 'chats-bbb';
const chat = 'chat-aaa-bbb';


function _invalidateServerKeys () {
    return new Promise((resolve, reject) => {
        pubnubSecret.grant({
            read: false,
            write: false,
            manage: false,
            ttl: 0
        }, (status, response) => {
            if (status.error) {
                reject(status);
            }
            resolve(status);
        });
    });
}


function _setMasterKeyForChannelGroup () {
    return new Promise((resolve, reject) => {
        pubnubSecret.grant({
            read: true,
            write: true,
            manage: true,
            channels: [chat],
            channelGroups: [userAGroup, userBGroup],
            authKeys: [pubnubConf.getMasterKey()],
            ttl: 0
        }, (status, response) => {
            if (status.error) {
                this._logger.fatal({
                    status: status,
                    response: response
                });
                reject(status);
            }
            resolve(status);
        });
    });
}

function _addChanelsToGroup () {
    return new Promise((resolve, reject) => {
        pubnub.channelGroups.addChannels({
            channels: [chat],
            channelGroup: [userBGroup]
        }, status => {
            if (status.error) {
                this._logger.fatal(status);
                return reject();
            }
            resolve(status);
        });
    });
}

function _addTokenPermission () {
    return new Promise((resolve, reject) => {
        pubnubSecret.grant({
            channelGroups: [userBGroup],
            authKeys: [token],
            ttl: 65,
            read: true,
            write: true,
            manage: false
        }, (status, response) => {
            if (status.error) {
                return reject({
                    status: status,
                    response: response
                });
            }
            resolve();
        });
    });
};

function _subscribeToGroup () {
    return new Promise((resolve, reject) => {
        pubnubClient.addListener({
            status: statusEvent => {
                if (statusEvent.error) {
                    reject(statusEvent);
                    return;
                }
                resolve(statusEvent);
            },
            message: message => {
            }
        });
        pubnubClient.subscribe(
            {channelGroups: [userBGroup]});
    });
}

function _publishToGroup () {
    pubnubClient.publish(
        {
            message: {
                such: 'object'
            },
            channel: chat,
            storeInHistory: false
        },
        function (status, response) {
            if (status.error) {
                // handle error
                console.log(status)
            } else {
                console.log("message Published w/ timetoken", response.timetoken)
            }
        }
    );
}

_invalidateServerKeys()
    .then(_setMasterKeyForChannelGroup)
    .then(_addChanelsToGroup)
    .then(_addTokenPermission)
    .then(_subscribeToGroup)
    .then(_publishToGroup);

const masterAuthKey = 'qdwqqdwdqwqdwqdw';


module.exports = {
    getSecretKeys: () => ({
        ssl: true,
        logVerbosity: true,
        publishKey: 'pub-c-',
        subscribeKey: 'sub-c-',
        secretKey: 'sec-c-'
    }),
    getMasterKeys: () => ({
        ssl: true,
        logVerbosity: true,
        publishKey: 'pub-c-',
        subscribeKey: 'sub-c-',
        authKey: masterAuthKey
    }),
    getMasterKey: () => (masterAuthKey)
};

当然,只有身份验证的 PAM 可以解决这个问题 - 但它似乎不可用 另一种方法是管理每个频道的服务器端密钥 - 但这有点浪费。

【问题讨论】:

    标签: node.js pubnub


    【解决方案1】:

    PubNub 访问管理器最佳实践

    这里有很多,所以我将只解决错误或不太准确或可以以更好的方式完成的事情。

    在全局级别撤销权限

    启用 Access Manager 时的默认设置是撤销所有人的所有权限。你在这里做什么:pubnubSecret.grant({read: false, write: false, manage: false, ttl: 0},只是撤销可能在子密钥(应用程序)级别授予的任何权限,但不会撤销任何身份验证密钥或通道级别(它们本质上是分层的,有点像CSS 覆盖但相反)。但它是无害的,实际上是一个很好的保护措施,以防某些人(内部开发人员)在子密钥级别意外授予。

    服务器需要有某种 masterKey

    ​​>

    您仅授予身份验证密钥的问题是预期行为,但我看到了您想要的 - 全局根访问身份验证密钥。目前,您可以为通道授予授权密钥或不授予授权密钥,但您不能向授权密钥授予nothing(无通道或通道组)权限。但是您可以在通道级别(无 auth-key)或子密钥级别(无通道和无 auth-key)授予访问权限,这样任何人都可以在没有 auth-key 的情况下访问(如上所述)。

    授予 root 访问权限以执行所有操作是一项缺失的功能,将在未来添加。现在你必须做两个不同的授权,让服务器拥有一个授权密钥,允许它读取、写入和管理所有内容,而无需为每个创建的新通道授权。

    通配符频道组管理

    首先,授予服务器的 auth-key 通配符权限,以使用 冒号 (:) 作为通配符管理所有频道组(不要问,但它与已弃用的命名空间有关以前的频道组)。

    pubnubSecret.grant({authKey: serverAuthKey, channelGroups:[':'], manage: true, ttl: 0}
    

    就是这样,您的服务器现在可以向您创建的任何频道组添加/删除频道。

    通配符通道读写

    对于频道,它有点不同,因为没有像管理频道组那样对所有频道通配符授予读/写权限 - 至少不是在 root 级别。我的意思是,如果您为所有频道名称使用频道前缀,例如notif.,这样频道的名称就会像notif.user123notif.user326 等,那么您可以授予您的读/写权限服务器在频道notif.* 上的身份验证密钥。

    pubnubSecret.grant({channels:['notif.*'], read: true, write: true, ttl: 0}
    

    现在这意味着,如果您有任何没有notif.* 前缀的频道,那么它们将不属于此通配符授权。而且您不能授予notif.user123.*。它仅适用于通配符名称的第二级/段。

    聊天频道和频道组

    您说您正在创建一个独特的频道供两个用户相互交谈:chat-{userId1}-{userId2}。根据上述建议,在它前面加上 notif. 前缀,以便您的服务器在需要这种访问权限时对其具有读/写访问权限。

    并且您表明您正在为每个用户创建频道组并将频道添加到每个用户的频道组,这将允许每个用户接收发布到该频道的消息。但是您错误地或错误地表示您正在尝试 publish 到频道组,但频道组仅用于订阅,而不是发布(即您不能 publish 到频道组)。并且永远不要将频道组上的manage 授予用户,因为这将允许恶意用户将任何频道添加到频道组并拥有read 访问该频道的权限。

    正确的做法是:

    1. 仅将manage 授予服务器的通道组 - 您已经使用带有冒号通配符的通配符通道组授予完成了此操作 - 所以,完成了。
    2. 授予read每个用户在自己的私人频道组上的访问权限:chats-{userId1}chats-{userId2}等,每个用户将订阅自己的频道组。
    3. 服务器将生成chat-user1-user2 频道并将该频道添加到每个用户的频道组,他们将立即订阅新的聊天频道。
    4. 服务器会将chat-user1-user2 频道的write 权限授予每个用户的auth-keys:auth-key-user1auth-key-user1。您可以将同一组频道的相同权限同时授予多个身份验证密钥。

    总结

    • 通配符授权manage 使用“:”将频道组授予服务器
    • 通配符将read/write 授予所有使用notif.* 频道到服务器的频道
    • read 授予用户私人频道组中的每个用户
    • write 授予共享聊天频道上的两个用户
    • 将共享聊天频道添加到每个用户的私人频道组

    我认为这几乎涵盖了您遇到的所有问题,但如果有任何不清楚的地方、未能解决您的问题中的某些问题或您还有其他问题,请告诉我。

    回复评论中的问题

    是'.'允许在频道名称中使用?

    是的,它不是官方无效的,而是保留。因此,请注意不要在不考虑通配符含义(授予和订阅)的情况下使用点字符作为分隔符。这就是为什么我在您的用例中推荐使用点,以便您可以授予 notifi.* 以使服务器对使用该命名约定创建的所有通道具有读/写访问权限。以下是有关保留/无效字符的一些其他详细信息。

    频道和频道组名称与 UTF-8 兼容,长度限制为 92 个字符。有一些保留字符不应该使用(但在某些情况下可以成功使用)作为频道或频道组名称。

    • 句号:'.' (适用于频道但不适用于频道组;仅应在计划使用通配符授权和/或wildcard subscribe 时在频道中使用)
    • 斜线:'/'
    • 反斜杠:'\'
    • 逗号:','
    • 冒号:':'
    • 空格:' '

    有生成频道名的命令吗?

    不,但是生成(可能是一个太强的术语)我的意思是您的服务器是创建频道名称的服务器。它几乎必须这样做,因为它需要知道该名称是什么,以便向客户端的 auth-keys 授予 write 权限,以便在其上发送 publish 消息。服务器也需要将频道添加到用户的频道组中。所以它也可能是创建频道名称的那个。

    如何生成频道名称取决于您。通常,开发人员只是使用 UUID 生成器来创建动态通道名称。我认为您正在寻找一个可预测的频道名称,以便每个客户都可以通过了解他们正在与之聊天的另一方的用户 ID 轻松知道频道名称是什么。请确保您按字典顺序对频道名称的用户 ID 进行排序,这样您就不会将它们倒退:chat-userabc-userxyz 而不是 chat-userxyz-userabc(您可能已经想通了,但为了所有阅读者的利益想提及它这)。如果您希望您的服务器具有自动 readwrite 访问权限(基于对 notif.* 建议的通配符授权),请不要忘记在频道名称前加上 notif. 前缀。

    【讨论】:

    • 不会猜测 ':',第二个是 '.'允许在频道名称中使用?因为它被指定在 API 中是不允许的,所以我认为还有一些其他字符,例如 ,/ 等等 - 这就是我选择使用连字符的原因。最后一个澄清'服务器将生成聊天用户1-用户2频道'是否有生成频道的命令?似乎只有 pub/sub 或添加到组
    • 在上面答案的最后回答了你的问题。
    • 只是为了验证 - 解决方案完美运行,还有一件事需要注意,不确定它是否有意 - 如果 pubnubClient 使用 authKey 初始化 - 在它实际获得权限之前 - 它失败 -我必须再次对其进行 .setAuthKey() 才能正常工作
    • 好吧,在授权密钥被授予做某事的权限之前,客户端不能做任何事情。但是,如果您使用 auth-key 进行初始化,然后服务器授予权限,则客户端可以在授予的通道上成功操作,而无需再次调用 setAuthKey。事实上,这通常是它在服务器端的工作方式:使用 pub/sub/secret/auth 密钥初始化,然后服务器使用该 auth-key 授予自己权限。
    • 如果您看到不同的行为,您可能试图在授权后过早(不到 1/2 秒)使用 auth-key。您应该在服务器上授予权限,将 auth-key 传递回客户端,使用 auth-key 进行初始化,然后开始使用被授予权限的操作来访问 PubNub。
    猜你喜欢
    • 2017-03-08
    • 2012-09-22
    • 1970-01-01
    • 1970-01-01
    • 2010-12-20
    • 2015-06-19
    • 2014-05-10
    • 1970-01-01
    相关资源
    最近更新 更多