推送
把服务端发出的 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 | 不支持 |
| Plus | 50,000 |
| Pro | 500,000 |
| Enterprise | 不限 |
额度用完后,Web Push 收件返回 429 (1009),应用后端发送逐条返回 quota_exceeded;推送地址不会失效,下月或升级后自动恢复。
准备推送凭据
推送凭据是账户级的,一份可被名下多个应用共用:
- Apple Developer → Certificates, Identifiers & Profiles → Keys,新建密钥并勾选 Apple Push Notifications service (APNs),下载
.p8文件 (只能下载一次) 并记下 Key ID - Team ID 在账号 Membership 页
- 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 版为falsealert:通知扩展解不开内容时显示的占位文字;正文缺省为New notificationkeys:可选,只有需要应用后端「按用户推送」时才提交,后端用它加密- 订阅 60 天没有刷新会被自动清理;建议 App 每次启动时 PUT 一次。PUT 收到
404说明订阅已不存在,重新登记即可
url 是这台设备的 Web Push 地址,交给服务端当推送地址。
待启用与验证
新登记的订阅处于「待启用」:这期间推来的消息只暂存 (每订阅最多 5 条、30 分钟),不弹通知。用于服务端的订阅验证,例如 JMAP 创建 PushSubscription 后会立即推一条加密的 PushVerification:
- App 在前台登记设备,把
url交给服务端创建推送订阅 - 轮询
GET /pending取到密文,在本地解密出验证码,回填给服务端 - 调
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 | 本月额度已用完 |
invalid | payload 不是合法 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 被省略时,系统显示登记的占位文字
限流
| 接口 | 维度 | 上限 |
|---|---|---|
| 设备登记 | IP | 20 次 / 小时 |
| 其余设备侧接口 | IP | 120 次 / 分钟 |
| 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 天未刷新时,订阅随即硬删除