ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Bitwarden server 推送通知系统深度解析:从 PushType 枚举到四种 IPushEngine 投递引擎

2026/9/13 11:43:59 拓冰建站 浏览量
Bitwarden server 推送通知系统深度解析:从 PushType 枚举到四种 IPushEngine 投递引擎 Bitwarden server 推送通知系统深度解析从 PushType 枚举到四种 IPushEngine 投递引擎【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server本文基于 Bitwarden server 仓库中 Push 模块设计文档 及其源码实现完整讲解这套面向终端用户设备的推送通知框架如何构造并发送一条PushNotificationT、如何安全地扩展新的通知类型、四种IPushEngine投递引擎Azure Notification Hub、Azure Queue、Relay、Notifications API各自的适用场景与依赖注入注册条件以及自托管与云托管两种部署形态下的完整通知投递链路。读完后你将在当前仓库中具备新增一种推送通知类型所需的完整实操能力并理解每条通知从 API 容器最终抵达手机或浏览器客户端的底层路径。1. Push 是什么核心定位与消息模型Push 是 Bitwarden server 中用于向终端用户设备发送信息包packet的功能。它的典型用途是告诉设备有新的信息需要主动拉取或你发起的某个请求刚刚被接受。例如密码库条目被创建/更新/删除、组织密钥变更、登录被请求Auth Request、账户需要登出等服务端都会通过这些通知让各端客户端及时同步。整个框架的消息模型由以下几个核心类型构成全部位于src/Core/Platform/Push/目录类型文件职责PushNotificationTPushNotification.cs承载一条通知的全部信息泛型T为负载类型PushTypePushType.cs通知类型枚举byte决定客户端侧路由到哪个处理器NotificationTargetNotificationTarget.cs通知目标枚举User/Organization/InstallationNotificationInfoAttributeNotificationInfoAttribute.cs标注每个PushType的负责团队与预期负载类型IPushNotificationServiceIPushNotificationService.cs对外统一的发送入口IPushEngineIPushEngine.cs各具体投递通道的引擎抽象NotificationTarget定义了三种可用目标User通知目标是单个用户TargetId填用户 IDOrganization通知目标是组织内所有用户TargetId填组织 IDInstallation通知目标是该安装installation下的所有组织及组织内所有用户TargetId填 installation ID。2. 发送一条推送IPushNotificationService.PushAsync用法日常使用中只需注入 IPushNotificationService 并调用其PushAsync方法传入一个PushNotificationT。以向某用户的所有设备发送请求已被接受通知为例// This would send a notification to all the devices of the given userId. await pushNotificationService.PushAsync(new PushNotificationMyPayload { Type PushType.MyNotificationType, Target NotificationTarget.User, TargetId userId, Payload new MyPayload { Message Request accepted, }, ExcludeCurrentContext false, });结合 PushNotification.cs 的源码各字段含义如下TyperequiredPushType枚举值。它用于把通知路由到客户端对应的处理器务必让负载类型与PushType关联的预期类型一致——这一点由PushType成员上的[NotificationInfo]特性以强制文档的方式约束见下文第 4 节。Target/TargetIdrequired目标实体类型及其 ID。PushNotification内部通过GetTargetWhen(NotificationTarget)辅助方法按目标类型取值例如目标为User时取TargetId作为用户 ID目标为Organization时作为组织 ID这在 Relay 引擎构造请求体时被用到见 RelayPushEngine.cs。Payloadrequired随通知发送的负载会被 JSON 序列化因此负载类型必须可 JSON 往返roundtrip。ExcludeCurrentContextrequired为true时通知不携带当前上下文标识符这意味着该通知可能恰恰由发起它的那个设备触发处理。各引擎在ExcludeCurrentContext为true时会从ICurrentContext中读取DeviceIdentifier写入通知见 AzureQueuePushEngine.cs 的GetContextIdentifier客户端据此排除自己。ClientType可选通知应发往的客户端类型为null时推断为ClientType.All。NonMobileOnly可选临时属性源码注释明确说明这是一个与 feature flag 绑定的临时属性为true时只有非移动端引擎SignalR/Web/桌面会投递该通知[EditorBrowsable(Never)]标记也表明它不应被新代码使用PushNotification.cs。另外注意接口文档中的一个重要语义PushAsync返回的Task不保证在任务完成时通知已经真正送达——这与第 6 节中release 构建不等待引擎的实现直接相关。IPushNotificationService的 XML 注释还给出了架构约定新通知不应在这个服务内部接线wire up你可以直接调用PushAsync也可以基于它编写带强类型定义的扩展方法或自己封装一个注入该服务的领域服务。接口上InstallationId、TimeProvider、Logger三个成员都标注了[Obsolete(BWP0001)]进一步强调业务方应走自己的服务而不是依赖这些暴露出来的成员。3. 扩展框架如何新增一个通知类型README 的 Extending 一节定义了向框架新增自有通知类型的标准流程源码与测试共同构成了这套流程的约束3.1 第一步给PushType枚举加成员打开 PushType.cs新增一个枚举成员数值取当前最大值加 1并必须用[NotificationInfo]特性标注负责团队和预期负载类型。当前枚举从SyncCipherUpdate 0到PremiumStatusChanged 27共 28 个成员例如现有的写法[NotificationInfo(bitwarden/team-billing-dev, typeof(Billing.Models.PremiumStatusPushNotification))] PremiumStatusChanged 27,NotificationInfoAttribute有两个构造重载一个接受Type一个接受负载类型的完整类型名字符串。接受字符串重载是刻意设计的——它允许团队为推送类型声明一个位于其他程序集中的负载类型而不必新增 using。属性注释中写明当前它只作为强制文档存在未来计划交给 C# analyzer 校验PushAsync调用点的负载类型是否正确。3.2 规则由单元测试强制执行PushType的三条硬性规则全部由 PushTypeTests.cs 中的单元测试守护数值唯一AllEnumMembersHaveUniqueValue不允许两个成员复用同一个 byte 值必须标注特性AllEnumMembersHaveNotificationInfoAttribute每个成员都必须有[NotificationInfo(team-name, typeof(MyType))]数值必须连续AllEnumValuesAreInSequence如果上一个最大定义是 22下一个必须用 23不允许跳号——这正是 README 所说Assign a number that is 1 above the next highest value的机器化保证。3.3 第二步在 HubHelpers 中补充分发逻辑新增通知类型后还需要在 HubHelpersNotifications 服务中添加代码读取你的负载体并决定把通知发给哪个用户或哪个组。从源码结构看SendNotificationToHubAsync对PushType做大 switch例如SyncCipherUpdate/SyncCipherCreate/SyncCipherDelete分支会反序列化为SyncCipherPushNotification若负载里有UserId就走_hubContext.Clients.User(...)发给该用户若有OrganizationId就走Clients.Group(GetOrganizationGroup(...))发给组织对应的 SignalR 组HubHelpers.cs。这是 Web/桌面/浏览器端经 SignalR 收到通知的实际分发点。3.4 测试与负载设计的两条约定不要在任何IPushEngine实现中为你具体的通知类型添加测试。这些引擎目前虽然还覆盖了许多通知类型的测试但那些测试后续会被删除且无需新增。由于自托管用户如果选择加入的通知会经由 Bitwarden 云实例中转负载信息应保持最小化。最佳实践是只发送相关实体的 ID——这些 ID 对云端毫无意义但设备收到通知后可凭 ID 拉取更详细的信息。4. 核心机制请求如何被散射到所有IPushEngineREADME Implementations 一节的机制描述可以精确对应到 MultiServicePushNotificationService.cs。该服务是 DI 中IPushNotificationService的唯一默认实现由 PushServiceCollectionExtensions.cs 注册构造时注入当前应用中所有已注册的IPushEngine// Filter out any NoopPushEngines _services [.. services.Where(engine engine is not NoopPushEngine)];PushAsync调用时PushToServices把同一条通知扇出scatter给每个引擎若没有任何可用引擎仅记录一条 No services found to push notification 警告并返回。源码中还能看到 README 所描述的release 构建不等待引擎的实现细节#if DEBUG var task pushFunc(service); tasks.Add(task); #else pushFunc(service); #endif ... #if DEBUG return Task.WhenAll(tasks); #else return Task.CompletedTask; #endif也就是说DEBUG 构建下PushAsync会Task.WhenAll等待所有引擎完成便于开发联调release 构建下不 await 任何引擎立即返回Task.CompletedTask——这解释了接口上返回的 Task 不保证通知已送达的语义。NoopPushEngine 作为内部空实现被过滤保证未配置任何真实通道时框架依然安全可用。5. 四种IPushEngine实现及其注册条件四种引擎的源码均位于src/Core/Platform/Push/Engines/与src/Core/Platform/Push/NotificationHub/目录而什么条件下注册哪个引擎的完整判据在 AddPush 扩展方法中5.1 Azure Notification Hub云托管移动端 Web Push适用场景应用由 Bitwarden 云托管时使用。通知被发送到 Azure Notification HubANH借助 ANH 与移动端推送系统的联邦能力触达移动客户端同时服务于配置了 Web Push 的客户端当前是 Chrome 扩展。注册条件云托管非 SelfHosted分支下无条件注册NotificationHubPushEngine配套NotificationHubPool单例并额外注册IPushRelayer该实现假定云端运行时配置必然可用。5.2 Azure Queue云托管SignalR/Web Sockets适用场景云托管环境下经 WebSocketSignalR投递通知。引擎把通知写入名为notifications的 Azure Queue该队列由 Notifications 服务消费再发送到 SignalR hub使通过持久 WebSocket 连接到通知服务的客户端收到通知。注册条件GlobalSettings:Notifications:ConnectionString有值时注册。源码中对应地注册了一个 keyedQueueClientkey 为notificationsAzureQueuePushEngine 通过[FromKeyedServices(notifications)]注入它并把PushNotificationDataT序列化为 JSON 后SendMessageAsync入队。注意此引擎与Installation.Id是否配置相关——未设置 Installation ID 时只会记录警告日志AzureQueuePushEngine.cs。5.3 Relay自托管经云中转适用场景自托管实例使用。由于自托管实例无法直接向移动设备发推送该引擎把通知从自托管实例中继relay到 Bitwarden 云实例云端接收后再转发给 Azure Notification Hub。注册条件GlobalSettings:PushRelayBaseUri与GlobalSettings:Installation:Key同时有值时注册。RelayPushEngine 继承BaseIdentityClientService携带ApiScopes.ApiPush作用域、以installation.{InstallationId}作为客户端身份向云 API 的push/send端点发起 POST该端点即 PushController。细节构造PushSendRequestModelT时用GetTargetWhen按三种NotificationTarget分别取UserId/OrganizationId/InstallationId并把当前设备的DeviceIdentifier与查询到的DeviceId一并写入请求当NonMobileOnly true时直接跳过该引擎负责移动端通道。5.4 Notifications API自托管直连 Notifications 服务适用场景自托管实例使用。引擎向自托管的 Notifications 服务发 API 请求后者收到后经 SignalR hub 发送通知。这与云端的 Azure Queue 路径非常相似但不要求自托管客户自己搭建队列基础设施。注册条件GlobalSettings:InternalIdentityKey与GlobalSettings:BaseServiceUri:InternalNotifications有值时注册。这两个设置在受支持的 Bitwarden 部署中通常自动配置。NotificationsApiPushEngine 基于内部身份internal.{ProjectName}客户端身份向内部端点send发起 POST负载同样是PushNotificationDataT。补充AddPush入口处还有一个前置校验——自托管模式下Installation.Id不能为空否则直接抛出InvalidOperationException(Installation Id must be set for self-hosted installations.)。汇总注册判据与 PushServiceCollectionExtensions.cs 一一对应引擎部署形态注册条件NotificationHubPushEngine云托管无条件非 SelfHosted 分支AzureQueuePushEngine云托管GlobalSettings:Notifications:ConnectionString有值RelayPushEngine自托管PushRelayBaseUriInstallation:Key有值NotificationsApiPushEngine自托管InternalIdentityKeyBaseServiceUri:InternalNotifications有值相关配置项定义可见 GlobalSettings.csPushRelayBaseUri、InternalIdentityKey、BaseServiceUri.InternalNotifications、Notifications等成员。6. 新增NotificationTarget为什么更难README Adding new notification targets 一节指出NotificationTarget是定义通知可用目标的枚举但新增一个目标不是加个枚举成员那么简单——各IPushEngine实现是否需要感知某个目标类型各不相同例如 ANH 实现依赖它来构造 tag 查询tag query因此新目标必须能通过 tag 查询表达。文档给了一个典型反例某团队想新增组织中已验证邮箱的用户这一目标。但今天这在目标层面无法表达——因为设备向 ANH 注册时并不携带该用户是否已验证邮箱。虽然理论上可以开始记录并同步这一信息用户验证邮箱时更新推送注册但追踪该信息并更新推送注册的成本需要与该通知的发送频率做权衡。更划算的替代方案通常是由需要的团队自行查询出符合条件的用户再逐个用NotificationTarget.User发送。如果此类需求足够多官方考虑的方案是给IPushNotificationService增加一个BulkPushAsync方法。NotificationTarget.cs 的源码注释也把协作边界写明了Please reach out to the Platform team if you need a new target added.——新增目标是需要与平台团队沟通的架构级变更而非自助操作。7. 两种部署形态下的完整投递链路README 末尾的两张 mermaid 图清晰刻画了自托管与云托管的通知链路此处完整保留7.1 自托管链路Self-host自托管实例中客户端动作到达 API 容器后分两路一路经 HTTP 调用本地 Notifications 容器再经 SignalR/Web Sockets 送达 Web、桌面、浏览器客户端另一路可选、可禁用经 HTTP 调用云 Push Relay由云端通过 ANH 库经 Firebase / APNS 触达 Android / iOS 移动端。对应第 5 节的引擎Notifications Container路径由NotificationsApiPushEngine驱动RelayPushEngine对应虚线的 Cloud Push Relay 分支可通过不配置PushRelayBaseUri禁用。7.2 云托管链路Cloud云托管中API 容器同时走两条独立通道直接向 ANH 发移动端与 Web Push 通知同时将通知入队 Azure Queue由 Notifications 容器出队后经 SignalR 送达 Web、桌面、浏览器客户端。对应引擎为NotificationHubPushEngineANH 通道与AzureQueuePushEngine队列通道队列消费端在 Notifications 服务src/Notifications/目录中的AzureQueueHostedService最终分发逻辑落在 HubHelpers。8. 小结与延伸阅读发送入口只有一个注入 IPushNotificationService 调PushAsync它会把通知散射到当前应用所有已注册的 IPushEnginerelease 构建不等待送达。扩展新通知类型是自助式的PushType加连续编号成员 [NotificationInfo]标注 HubHelpers 补分发 负载保持最小化只放 ID规则由 PushTypeTests 强制。扩展新NotificationTarget是平台级变更受 ANH tag 查询表达能力约束需要联系平台团队并权衡注册信息追踪成本。引擎选择完全由 AddPush 中的GlobalSettings配置决定云托管默认 ANH 可选 Azure Queue自托管为 Relay经云中转移动端与 Notifications API本地 SignalR二者注册互不冲突、可同时启用。可进一步深入的文件src/Core/Platform/Push/NotificationHub/NotificationHubPool.csANH 客户端池、src/Notifications/AzureQueueHostedService.cs队列消费端、src/Api/Platform/Push/Controllers/PushController.cs云端接收 relay 请求的端点、src/Core/Platform/PushRegistration/设备向 ANH 注册推送的配套机制。【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考