Web Push cho phép server đánh thức Service Worker và gửi thông tin khi website không mở. Nhưng một hệ thống production không chỉ là gọi pushManager.subscribe(): phải quản lý consent, VAPID key, payload encryption, endpoint hết hạn, TTL, retry và chống gửi quá mức.
Bài viết triển khai luồng hoàn chỉnh từ browser đến application server và push service, dựa trên W3C Push API, RFC 8030 Web Push, RFC 8291 Message Encryption và RFC 8292 VAPID.
1. Kiến trúc gồm ba bên
- Web app/browser: đăng ký Service Worker, xin permission và tạo push subscription.
- Application server: lưu subscription, chọn người nhận, mã hóa payload và ký VAPID.
- Push service: nhận HTTP request từ server, lưu tạm theo TTL và chuyển message tới user agent.
Push service không phải API backend của ứng dụng. Server gửi tới endpoint do browser cấp, không gửi thẳng tới thiết bị. Message được giao cho Service Worker gắn với scope của subscription; worker có thể cập nhật state hoặc hiển thị notification.
2. Xin permission đúng thời điểm
Đừng mở permission prompt ngay khi trang vừa load. Hãy giải thích giá trị cụ thể—ví dụ “báo khi đơn hàng thay đổi trạng thái”—rồi chỉ xin quyền sau một user gesture rõ ràng.
async function enableNotifications() {
if (!("serviceWorker" in navigator) ||
!("PushManager" in window)) {
return { supported: false };
}
const permission = await Notification.requestPermission();
if (permission !== "granted") {
return { supported: true, permission };
}
return { supported: true, permission };
}
Phân biệt default với denied. Khi denied, không lặp prompt hoặc dùng dark pattern. Cung cấp trang settings và hướng dẫn người dùng tự thay đổi quyền ở browser.
3. Đăng ký Service Worker có scope rõ ràng
const registration = await navigator.serviceWorker.register(
"/push-worker.js",
{ scope: "/" }
);
await navigator.serviceWorker.ready;
Service Worker yêu cầu secure context (HTTPS, ngoại trừ môi trường phát triển được browser cho phép). File worker phải được phục vụ đúng MIME type, không cache quá lâu khi đang rollout và scope phải bao phủ trang mà notification sẽ mở.
Update worker là deployment riêng: giữ handler cũ tương thích với payload mới trong cửa sổ rollout, vì thiết bị có thể chạy các phiên bản worker khác nhau.
4. VAPID key là danh tính của application server
Tạo một cặp khóa P-256 VAPID bằng thư viện Web Push đáng tin cậy. Public key được gửi xuống browser; private key chỉ ở server/secret manager.
VAPID JWT dùng ES256 và thường chứa:
aud: origin của push service lấy từ subscription endpoint;exp: thời điểm hết hạn, không quá 24 giờ theo RFC;sub: URI liên hệ dạng HTTPS hoặcmailto:.
Đừng tạo key mới mỗi lần deploy. Subscription có thể bị ràng buộc với application-server key đã dùng lúc đăng ký; rotate key cần giai đoạn song song hoặc yêu cầu client subscribe lại.
5. Tạo subscription ở browser
function base64UrlToUint8Array(value) {
const padding = "=".repeat((4 - value.length % 4) % 4);
const base64 = (value + padding)
.replace(/-/g, "+")
.replace(/_/g, "/");
const raw = atob(base64);
return Uint8Array.from(raw, c => c.charCodeAt(0));
}
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: base64UrlToUint8Array(VAPID_PUBLIC_KEY),
});
}
await fetch("/api/push/subscriptions", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(subscription.toJSON()),
});
Dùng getSubscription() trước để thao tác idempotent. Endpoint đăng ký phía server phải yêu cầu phiên đăng nhập/CSRF protection phù hợp; không cho client gắn subscription tùy ý vào user khác.
6. Subscription chứa gì và phải bảo vệ ra sao?
{
"endpoint": "https://push.example/...",
"expirationTime": null,
"keys": {
"p256dh": "...",
"auth": "..."
}
}
p256dh và auth phục vụ mã hóa payload; private key tương ứng nằm trong user agent và không được cung cấp cho ứng dụng. Endpoint là capability URL: ai có URL có thể thử gửi tới subscription nếu vượt qua ràng buộc VAPID. Không log đầy đủ endpoint hoặc đưa vào analytics/client error report.
Trong database, lưu endpoint theo encrypted column hoặc storage access chặt, hash riêng để deduplicate/lookup, cùng user ID, device label, locale, created/last-success/last-error và trạng thái revoked. Không dùng endpoint làm session token.
7. Gửi message: dùng thư viện chuẩn
Application server phải:
- serialize payload nhỏ;
- mã hóa theo RFC 8291 với key
p256dhvàauth; - tạo VAPID authorization cho đúng push-service origin;
- gửi POST tới endpoint với TTL và content encoding phù hợp.
Đây là crypto/protocol code không nên tự viết. Dùng thư viện Web Push được duy trì cho ngôn ngữ của bạn, pin phiên bản và test interoperability với các browser mục tiêu.
webPush.sendNotification(subscription, JSON.stringify({
type: "order.updated",
orderId: "ord_123",
version: 4
}), {
TTL: 300,
urgency: "normal",
topic: "order-ord_123"
});
Phản hồi HTTP 2xx chỉ cho biết push service chấp nhận message, không chứng minh thiết bị đã hiển thị hoặc người dùng đã đọc.
8. Payload nên là tín hiệu, không phải kho dữ liệu
Dù payload được mã hóa end-to-end giữa application server và user agent, hãy giữ nhỏ và tối thiểu:
- gửi event type, object ID và version;
- để Service Worker hoặc trang fetch dữ liệu mới sau khi mở;
- không chứa password, access token, medical/financial detail nhạy cảm;
- không tin payload như authorization; API vẫn kiểm tra session/quyền;
- version schema để worker cũ có thể bỏ qua field mới.
Push service thấy metadata giao vận như endpoint, thời điểm, kích thước, TTL và urgency dù không đọc được payload plaintext.
9. Xử lý push event bằng waitUntil
self.addEventListener("push", event => {
let message = { type: "generic" };
try {
if (event.data) message = event.data.json();
} catch (_) {
message = { type: "generic" };
}
const title = message.type === "order.updated"
? "Đơn hàng vừa được cập nhật"
: "Bạn có cập nhật mới";
event.waitUntil(self.registration.showNotification(title, {
body: "Mở ứng dụng để xem chi tiết.",
tag: message.type + ":" + (message.orderId || "general"),
data: { path: "/orders/" + encodeURIComponent(message.orderId || "") },
icon: "/icons/push-192.png",
}));
});
event.waitUntil() giữ worker sống đến khi Promise hoàn tất. Validate schema, đặt fallback và không render HTML từ payload. Dùng tag để thay thế notification cùng chủ đề thay vì tạo một hàng dài.
10. Notification click phải giới hạn navigation
self.addEventListener("notificationclick", event => {
event.notification.close();
const path = event.notification.data?.path || "/notifications";
const target = new URL(path, self.location.origin);
if (target.origin !== self.location.origin) return;
event.waitUntil((async () => {
const windows = await clients.matchAll({
type: "window",
includeUncontrolled: true,
});
for (const client of windows) {
if ("focus" in client) {
await client.navigate(target.href);
return client.focus();
}
}
return clients.openWindow(target.href);
})());
});
Không mở URL tuyệt đối từ payload mà không allowlist origin/path; đó là open-redirect/phishing surface. Trang đích vẫn phải xác thực user và authorization.
11. TTL, urgency và topic
- TTL: thời gian push service được giữ message khi user agent offline. OTP sắp hết hạn cần TTL ngắn; tin tổng hợp có thể dài hơn.
- Urgency: gợi ý mức ưu tiên, ảnh hưởng scheduling/power nhưng không bảo đảm latency.
- Topic: cho phép message mới thay message pending cũ cùng topic tại push service.
Đừng dùng urgency cao cho marketing. Chọn topic ổn định như trạng thái của một order hoặc unread-count để coalesce update. Web Push không phải ordered queue: message có thể trễ, thay thế hoặc không đến; payload và API phải idempotent.
12. Dọn subscription chết và retry đúng loại lỗi
| Phản hồi | Hành động |
|---|---|
2xx | Đã được push service chấp nhận; cập nhật last accepted. |
404/410 | Endpoint không còn hợp lệ; xóa/revoke subscription, không retry. |
413 | Payload quá lớn; giảm payload, không retry nguyên trạng. |
429 | Rate limited; tôn trọng Retry-After và exponential backoff. |
5xx | Lỗi tạm thời; retry có deadline, jitter và giới hạn. |
Subscription có thể refresh và thay key/endpoint. Đồng bộ lại khi app mở, xử lý pushsubscriptionchange khi được browser hỗ trợ và thiết kế API upsert theo user+device. Cho người dùng nút unsubscribe; gọi subscription.unsubscribe() và revoke record server.
13. Chống spam và đo lường có trách nhiệm
- Preference theo loại sự kiện, quiet hours và timezone.
- Frequency cap cho từng user/device, không chỉ toàn campaign.
- Deduplicate bằng event ID và topic.
- Không gửi nội dung nhạy cảm lên lock screen.
- Không dùng silent push để tracking nền hoặc duy trì worker trái kỳ vọng.
- Đo accepted, invalid endpoint, throttle, click và unsubscribe; không gọi accepted là delivered.
- Redact endpoint, auth key và payload khỏi log/tracing.
Push là quyền người dùng có thể rút lại. Chất lượng permission funnel và tỷ lệ unsubscribe thường quan trọng hơn số subscription thô.
Checklist production
- Permission chỉ xin sau giải thích và user gesture.
- VAPID private key nằm trong secret manager; public key ổn định.
- Subscription API có authentication, CSRF defense và ownership check.
- Endpoint và encryption keys được redaction khỏi log.
- Dùng thư viện RFC 8291/8292 được duy trì, không tự viết crypto.
- Payload nhỏ, versioned, không chứa secret và không thay authorization.
- Service Worker dùng waitUntil, fallback và same-origin navigation.
- TTL, urgency, topic phản ánh semantics thật.
- 404/410 bị xóa ngay; 429/5xx retry hữu hạn có jitter.
- Có unsubscribe, quiet hours, frequency cap và dashboard lỗi theo push service.
Kết luận
Web Push là một pipeline bất đồng bộ gồm browser, push service và application server. VAPID xác thực phía gửi, RFC 8291 bảo vệ payload, còn subscription endpoint định tuyến message tới đúng user agent. Khi consent được tôn trọng, khóa được quản lý ổn định, payload tối thiểu và endpoint chết được dọn tự động, Web Push trở thành kênh cập nhật hữu ích thay vì nguồn spam và lỗi vận hành.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.