Web Push(iOS PWA)消息推送

附件是一套完整的 Web Push 推送演示,支持 iPhone「添加到
主屏幕」后接收推送(iOS 16.4+),也支持 Android/桌面 Chrome。
无需数据库,单个 PHP 文件实现全部后端逻辑(RFC8291 负载加密、
RFC8292 VAPID 签名),自带管理端发送页面。

【文件清单】
index.php 后端主程序(API 路由 / 加密 / VAPID /
动态生成 sw.js、manifest、图标)
app.html 接收端页面(iPhone 订阅用,路径 /app)
send.html 管理端页面(发送通知,路径 / )
sw.js Service Worker(静态文件,必须存在!)
icon.png 通知/应用图标 192×192
apple-touch-icon.png iOS 主屏幕图标
favicon.ico 网站图标
data/store.json 运行时自动生成:VAPID 密钥 + 订阅列表
(含私钥,切勿外泄,禁止公网下载)

【运行要求】
1. PHP >= 7.3,需启用 openssl、curl 扩展(宝塔默认已启用)
2. 必须 HTTPS(Service Worker / Web Push 的硬性要求)
3. iPhone 接收推送需 iOS 16.4+,且页面必须以「主屏幕 PWA」
方式运行(Safari 普通标签页没有通知能力)
4. 服务器需能访问外网推送服务(web.push.apple.com、
fcm.googleapis.com 等)

================================================================
一、宝塔面板部署(推荐)
================================================================

1. 创建站点
宝塔面板 → 网站 → 添加站点:
– 域名:填你的域名(如 push.example.com)
– PHP版本:选 7.3 以上(推荐 8.x),不要选「纯静态」
– 数据库:不创建

2. 上传文件
将本项目全部文件上传到站点根目录:
/www/wwwroot/你的站点目录/
保持文件结构不变,sw.js、icon.png 必须在根目录。

3. 设置伪静态(关键!)
宝塔面板 → 网站 → 设置 → 伪静态,填入:

location / {
try_files $uri /index.php;
}
# 禁止外部下载运行时数据(内含推送私钥和订阅列表)
location ^~ /data/ {
deny all;
}

说明:try_files 让 /app、/api/*、/manifest.webmanifest 等
虚拟路径都交给 index.php 处理。

4. 申请 SSL 证书并开启强制 HTTPS
宝塔面板 → 网站 → 设置 → SSL → Let’s Encrypt 申请
→ 开启「强制HTTPS」。

5. 检查目录权限
确保 PHP 进程(www 用户)对站点目录有写权限,
data/store.json 需要自动创建:
chown -R www:www /www/wwwroot/你的站点目录
chmod 755 /www/wwwroot/你的站点目录

6. 验证部署(浏览器依次访问)
https://你的域名/ → 应显示「推送管理端」页面
https://你的域名/app → 应显示「Web Push 接收端」
https://你的域名/sw.js → 应返回 JS 代码(HTTP 200)
https://your域名/manifest.webmanifest → 应返回 JSON

★ 重要提醒(宝塔特有的大坑):
宝塔默认 nginx 配置含 location ~ .*\.(js|css)?$ 规则,
所有 .js 请求会被拦截为纯静态文件,不会走 PHP。
因此 sw.js 必须以真实静态文件存在于网站根目录
(本包已提供)。若修改了 index.php 里的 SW 逻辑,
记得同步更新静态 sw.js,否则浏览器注册的还是旧文件。
同理 icon.png / apple-touch-icon.png / favicon.ico
也是静态文件(本包已提供)。

================================================================
二、纯 nginx 手动部署(不用宝塔)
================================================================

假设:站点目录 /www/wwwroot/webpush,域名 push.example.com,
PHP-FPM 监听 unix:/run/php/php8.2-fpm.sock(按实际修改)。

1. 安装 nginx + PHP(>=7.3,带 openssl、curl 扩展)+ PHP-FPM

2. 上传项目文件到 /www/wwwroot/webpush

3. nginx 配置示例(/etc/nginx/conf.d/webpush.conf):

server {
listen 80;
server_name push.example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl http2;
server_name push.example.com;

ssl_certificate /etc/nginx/certs/push.example.com.pem;
ssl_certificate_key /etc/nginx/certs/push.example.com.key;

root /www/wwwroot/webpush;
index index.php;

# 虚拟路径交给 PHP 路由
location / {
try_files $uri /index.php;
}

# 保护运行时数据(内含私钥)
location ^~ /data/ {
deny all;
}

# PHP-FPM
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}

# 其余静态文件(sw.js / 图标)由 nginx 直接返回,
# 与 PHP 动态输出内容保持一致即可
}

4. 检测并重载 nginx:
nginx -t && nginx -s reload

5. 按上面「宝塔部署」第 6 步验证。

(可选)极简部署——不装 nginx,用 PHP 内置服务器:
php -S 0.0.0.0:8090 index.php
此模式下 sw.js/manifest/图标全部由 index.php 动态输出,
无需静态文件。但生产环境不建议,且仍需 HTTPS
(可用 frp/caddy 等做 TLS 终结)。

================================================================
三、使用说明
================================================================

┌─────────────── 接收端(iPhone)───────────────┐
│ │
│ 1. 系统要求 iOS 16.4 及以上 │
│ 2. 用 Safari 打开 https://你的域名/app │
│ 3. 分享 → 添加到主屏幕 │
│ 4. 从主屏幕图标打开(不能用 Safari 标签页) │
│ 5. 点「开启推送通知」→ 系统弹窗 → 允许 │
│ 6. 页面显示:通知权限=已授权、Service Worker= │
│ 已注册、推送订阅=已订阅,即订阅成功 │
│ │
└────────────────────────────────────────────────┘

┌─────────────── 管理端(发送)─────────────────┐
│ │
│ 1. 任意设备浏览器打开 https://你的域名/ │
│ 2. 填写标题、内容 → 点「发送」 │
│ 3. 所有已订阅设备会收到系统通知 │
│ │
└────────────────────────────────────────────────┘

【接收通知的正确姿势(iOS 特性)】
– iPhone 上把 PWA 切到后台或锁屏,再从管理端发送;
PWA 在前台时 iOS 会抑制推送横幅。
– 若锁屏不弹但通知中心(顶部下拉)里有记录:
设置 → 通知 → 找到该应用 → 开启「允许通知」,
勾选锁定屏幕/通知中心/横幅;
若通知为「静默送达」,长按该通知 → 选「突出显示」。
– 配对了 Apple Watch 且戴在手腕上时,锁屏通知会转到
手表显示,手机不弹属正常现象。
– 开启了专注模式(勿扰等)时通知可能被静默。

【管理端 API(可供程序调用)】
– GET /api/config 获取 VAPID 公钥和订阅数
– POST /api/subscribe 上报订阅
body: {“subscription”:{endpoint,keys:{p256dh,auth}}}
– POST /api/send 发送通知
body: {“title”:”标题”,”body”:”内容”,”url”:”/app”}
(title 和 body 不能同时为空;url 为通知点击后打开的页面)

【数据与密钥】
– VAPID 密钥对首次运行时自动生成,保存在 data/store.json。
– 若删除 data/store.json,会生成新密钥,此时所有旧订阅全部
失效(推送服务校验 VAPID 公钥一致性),设备需重新订阅。
– 建议把 index.php 顶部的 VAPID_SUBJECT 改成你自己的邮箱。

【订阅生命周期】
– 设备每次点「重新订阅」都会产生新订阅;旧订阅失效后推送
服务返回 404/410,后端会自动从 store.json 中清理。

================================================================
四、常见问题排查(FAQ)
================================================================

Q1: 报「Script sw.js load failed」
→ 浏览器访问 https://域名/sw.js 看是否 200。
→ 常见原因:nginx 把 .js 拦截为静态文件但文件不存在,
确认根目录有真实 sw.js 文件。

Q2: 管理端报「Unexpected token ‘<‘ … is not valid JSON」
→ PHP 报错信息混入了 JSON 输出。
→ 排查:临时在 index.php 开头加
ini_set(‘display_errors’, ‘0’);
→ 查看具体报错:看 nginx/PHP 错误日志。
→ 宝塔常见诱因:open_basedir 限制触发 Warning
(本版本已用 @is_file() 修复)。

Q3: 发送返回 201 但手机收不到
→ 201 只代表苹果/谷歌推送服务器「接收」了消息,不代表
设备能解密显示。
→ 先排除手机设置问题(见「接收通知的正确姿势」)。
→ 若加密实现不符合 RFC8291,设备会静默丢弃且无任何报错
(本版本已通过 http_ece 参考实现交叉验证修复:
key_info = “WebPush: info\0” || ua公钥 || 服务器公钥)。

附件下载

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注