推送

把服务端发出的 Web Push (RFC 8030) 和应用后端的消息转成 iOS 系统通知 (APNs)。适合只会发 Web Push 的服务端 (如邮件服务器的 JMAP 推送订阅),也适合自己的后端给指定用户推通知。

内容端到端加密:发送方用设备公钥按 RFC 8291 (aes128gcm) 加密,CZL Connect 只经手密文,原样放进通知的自定义键 c,由 App 的通知扩展解密后显示。CZL Connect 看不到通知内容。

适用场景

  • 邮件、日历等只支持 Web Push 的服务端,要给 iOS App 发新消息通知
  • 应用后端按用户推通知 (订单状态、消息提醒),一个用户的多台设备都能收到
  • 多个 App 共用一份 Apple 推送密钥,不想各自部署推送服务

开通条件

推送按应用所有者的套餐开放,额度为账户内所有应用合计,按每月成功交给推送平台的通知条数计 (待启用期间的暂存与发送失败不计):

套餐每月推送条数
Lite不支持
Plus50,000
Pro500,000
Enterprise不限

额度用完后,Web Push 收件返回 429 (1009),应用后端发送逐条返回 quota_exceeded;推送地址不会失效,下月或升级后自动恢复。

准备推送凭据

推送凭据是账户级的,一份可被名下多个应用共用:

  1. Apple Developer → Certificates, Identifiers & Profiles → Keys,新建密钥并勾选 Apple Push Notifications service (APNs),下载 .p8 文件 (只能下载一次) 并记下 Key ID
  2. Team ID 在账号 Membership 页
  3. CZL Connect → 账户设置 → 集成 → 推送凭据 → 添加凭据,填入 Key ID、Team ID 并粘贴 .p8 全部内容

保存前会用凭据实际连一次推送平台,被拒绝则无法保存。密钥加密存储,保存后不再回显。一份 .p8 可给同一开发者团队下所有 App 推送,沙盒与正式环境通用。凭据之后若被吊销,发送失败原因为 credential_rejected,凭据卡片与应用推送分区都会提示。

应用中开通

进入应用详情 → 推送 (仅应用所有者可见):

设置说明
开通推送关闭后设备登记与推送都会被拒绝,已登记设备保留
推送凭据从账户的推送凭据中选择
App 标识iOS 的 Bundle ID,每行一个。全站唯一,保存时用所选凭据校验它属于凭据所在团队;移除的标识连同其设备订阅一起删除

保存需要通行密钥确认,并记入操作日志。

设备侧接口

基址:https://connect.czl.net/api/push (以应用推送分区「接入地址」为准)。设备登记时拿到的 secret 只返回一次,之后用 Authorization: Bearer <secret> 调用。

方法路径说明
POST/v1/subscriptions登记设备 → 201 {id, secret, url}
PUT/v1/subscriptions/{id}换设备令牌 / 占位文字,并刷新活跃时间 → 204
GET/v1/subscriptions/{id}/pending取待启用期间暂存的密文 → {messages: [base64url…]}
POST/v1/subscriptions/{id}/activate启用,清空暂存 → 204
DELETE/v1/subscriptions/{id}删除 → 204

登记 / 刷新的请求体:

{
  "topic": "com.example.app",
  "token": "设备令牌 (hex)",
  "sandbox": false,
  "alert": { "title": "Example", "body": "新消息" },
  "keys": { "p256dh": "设备公钥 (base64url)", "auth": "auth secret (base64url)" }
}
  • topic 须是已在某个开通了推送的应用中登记的 App 标识,否则返回 1008 unknown app;刷新时不可修改
  • sandbox:开发版 (Xcode 直接安装) 为 true,TestFlight 与 App Store 版为 false
  • alert:通知扩展解不开内容时显示的占位文字;正文缺省为 New notification
  • keys:可选,只有需要应用后端「按用户推送」时才提交,后端用它加密
  • 订阅 60 天没有刷新会被自动清理;建议 App 每次启动时 PUT 一次。PUT 收到 404 说明订阅已不存在,重新登记即可

url 是这台设备的 Web Push 地址,交给服务端当推送地址。

待启用与验证

新登记的订阅处于「待启用」:这期间推来的消息只暂存 (每订阅最多 5 条、30 分钟),不弹通知。用于服务端的订阅验证,例如 JMAP 创建 PushSubscription 后会立即推一条加密的 PushVerification:

  1. App 在前台登记设备,把 url 交给服务端创建推送订阅
  2. 轮询 GET /pending 取到密文,在本地解密出验证码,回填给服务端
  3. 调 POST /activate,之后的推送才会转成系统通知

不需要验证的场景,登记后直接调 activate。

服务端的推送订阅通常有有效期,例如 Stalwart 的 JMAP PushSubscription 默认 7 天过期:App 应在回到前台时与定期 (如每 12 小时) 向服务端续期 (更新 expires),同时 PUT 一次本服务的订阅刷新活跃时间。续期不改变推送地址,无需重新登记或验证;只有设备密钥变了 (重装 App) 或本服务返回 404 时才重建。

Web Push 收件

POST {url} (即 /api/push/p/{id}),正文为 RFC 8291 密文,不超过 8 KB。Urgency: low / very-low 走省电优先级,系统可能攒着一起送。

状态含义
201已受理 (启用前为已暂存)
404订阅不存在,服务端应停用该地址
410设备令牌已失效 (App 卸载等),订阅已删除,服务端应停用该地址
413消息过大
429本月额度已用完 (1009) 或请求过于频繁 (1006),稍后重试
403应用推送已停用 (1010),恢复后地址继续可用
502投递失败 (1007),如凭据不可用、推送平台暂时不可用

按用户推送 (应用级 API)

应用后端用 应用级 API 凭证调用,需要 scope push:send:

1. 绑定设备到用户:App 把自己的订阅 id 与 secret 经应用自己的登录通道交给后端,后端调用

POST /api/v1/app/push/subscriptions/bind
{ "subscriptionId": "...", "secret": "...", "userId": "123" }

userId 为本应用用户在 CZL Connect 的用户 ID,该用户须已授权本应用。用户在 App 内登出时调 DELETE /api/v1/app/push/subscriptions/{id}/bind 解绑,订阅保留为匿名。

2. 列出用户的设备:GET /api/v1/app/push/users/{userId}/devices → {devices: [{id, platform, sandbox, keys: {p256dh, auth}, createdAt, lastPushAt}]},只含已启用的设备。

3. 逐台加密后批量发送:

POST /api/v1/app/push/send
{ "messages": [ { "subscriptionId": "...", "payload": "密文 (base64url)", "urgency": "high" } ] }

单次最多 100 条,逐条返回 {subscriptionId, status, reason}:

status含义
sent已交给推送平台
gone设备令牌已失效,订阅已删除,不要再发
failed发送失败,reason 为 credential_missing / credential_rejected / provider_error
skipped用户退订了本应用的通知、被封禁或在本应用中被停用
inactive订阅尚未启用
not_found订阅不存在或不属于本应用
quota_exceeded本月额度已用完
invalidpayload 不是合法 base64url 或超过 8 KB

用户退订的粒度是「该应用的通知」,与群发邮件共用;匿名订阅 (未绑定用户) 不受影响。

App 端要求

  • 申请通知权限后取设备令牌,按开发版 / 正式版设置 sandbox
  • 生成 P-256 密钥对与 16 字节 auth secret,私钥存在主 App 与通知扩展共享的钥匙串 (App Group),公钥与 auth 交给服务端 (Web Push 订阅) 或登记时提交 (keys)
  • 添加 Notification Service Extension:通知带 mutable-content: 1,扩展读取自定义键 c (base64url 密文),按 RFC 8291 解密后改写标题正文;解不开或密文超过约 4 KB 被省略时,系统显示登记的占位文字

限流

接口维度上限
设备登记IP20 次 / 小时
其余设备侧接口IP120 次 / 分钟
Web Push 收件订阅120 次 / 分钟
应用级 API应用与其它应用级 API 共用

超限返回 429,错误码 1006。

错误码

设备侧与 Web Push 收件的报错为 {"error": "<4 位码> <英文说明>"}:

码含义
1001参数无效 (如设备令牌格式错误)
1002缺少凭据 (未带 Authorization)
1003订阅不存在 / 已失效
1004消息过大
1005存储错误
1006请求过于频繁
1007投递失败
1008未登记的 App
1009本月推送额度已用完
1010该应用推送已停用

数据与隐私

  • 订阅保存设备令牌、secret 的 SHA-256 与可选的设备公钥;通知内容不落库,待启用暂存的密文 30 分钟后自动丢弃
  • 只记录按天汇总的发送计数,不记录逐条发送记录
  • 令牌失效、订阅删除、应用删除或 60 天未刷新时,订阅随即硬删除