你有没有想过,为什么有些网站能在你没打开它们的时候,也给你发通知?比如,新闻网站推送突发新闻,或者购物网站提醒你购物车里的商品降价了。这背后,有一个重要的技术叫做 Web Push(网页推送),而 VAPID,就是保证 Web 推送安全可靠的“门卫”。
什么是 Web Push?
Web Push 是一种允许网站向用户发送通知的技术,即使用户没有打开该网站。它基于浏览器的 Service Worker 和 Push API。
- Service Worker: 这是一个在后台运行的脚本,独立于网页。它可以拦截网络请求、处理推送消息等。
- Push API: 这是浏览器提供的一组接口,允许 Service Worker 接收来自服务器的推送消息。
想象一下,Service Worker 就像一个 24 小时值班的“邮递员”,住在你家(浏览器)附近。网站(比如新闻网站)可以把信件(推送消息)交给“邮局”(推送服务),“邮递员”收到后,就会把信件送到你家门口(显示通知)。
VAPID:安全推送的“钥匙”
但是,问题来了。如果没有一个安全的机制,任何网站都可以假冒其他网站给你发推送,那你的通知栏岂不是要被垃圾信息淹没了?
这时候,VAPID 就派上用场了。VAPID 的全称是 Voluntary Application Server Identification,翻译过来就是“自愿应用程序服务器标识”。
你可以把 VAPID 理解成一把“钥匙”:
- 公钥(Public Key):这把“钥匙”是公开的,网站会把它交给浏览器。浏览器拿着这把“钥匙”,去“邮局”(推送服务)订阅推送。
- 私钥(Private Key):这把“钥匙”只有网站自己知道,用来给推送消息“签名”。
当网站要发送推送消息时,它会用私钥对消息进行“签名”。“邮局”(推送服务)收到消息后,会用网站之前提供的公钥来验证签名。如果签名正确,说明这条消息确实是来自这个网站的,就可以安全地发送给用户了。
这样,就保证了只有经过网站授权的推送消息才能发送给用户,防止了恶意推送和信息伪造。
VAPID 的工作原理
- 生成 VAPID 密钥对:网站首先要生成一对 VAPID 密钥(公钥和私钥)。
- 订阅推送:用户访问网站时,网站会请求用户授权接收推送。如果用户同意,浏览器会向推送服务(Push Service)订阅推送,并提供 VAPID 公钥。
- 保存订阅信息:推送服务会返回一个订阅对象(Subscription),其中包含了推送的端点(endpoint)和一个唯一的加密密钥(p256dh)和身份验证密钥(auth)。网站需要将这些信息保存到数据库中。
- 发送推送消息:当网站需要发送推送消息时,它会从数据库中取出用户的订阅信息,使用 VAPID 私钥对消息进行签名,并将消息发送到订阅信息中的端点。
- 推送服务验证签名:推送服务收到消息后,会使用 VAPID 公钥验证签名。如果签名有效,推送服务会将消息发送给用户的浏览器。
- 浏览器显示通知:用户的浏览器收到消息后,Service Worker 会处理消息,并在通知栏中显示通知。
如何生成 VAPID 密钥对?
你可以使用一些工具或库来生成 VAPID 密钥对。这里介绍两种常用的方法:
1. 使用在线工具
有一些在线工具可以帮助你生成 VAPID 密钥对,比如 https://vapidkeys.com/。
你只需要打开这个网站,点击“Generate”按钮,就可以生成一对密钥。这种方法简单快捷,适合快速测试。
2. 使用 Node.js 的 web-push
库
如果你是 Node.js 开发者,可以使用 web-push
库来生成 VAPID 密钥对。首先,你需要安装 web-push
:
npm install web-push -g
然后,你可以使用以下命令生成密钥对:
web-push generate-vapid-keys
这条命令会在控制台中输出生成的公钥和私钥。请妥善保管私钥,不要泄露。
生成的密钥是 URL 安全的 Base64 编码的字符串。
代码示例 (Node.js)
下面是一个使用 Node.js 和 web-push
库实现 Web 推送的简单示例:
1. 生成 VAPID 密钥对 (已经讲过,不再重复)
2. 前端代码 (index.html)
<!DOCTYPE html>
<html>
<head>
<title>Web Push Example</title>
</head>
<body>
<h1>Web Push Example</h1>
<button id="subscribe">订阅推送</button>
<script>
const subscribeButton = document.getElementById('subscribe');
// 检查浏览器是否支持 Service Worker
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/service-worker.js') // 注册 Service Worker
.then(function(registration) {
console.log('Service Worker 注册成功:', registration);
subscribeButton.addEventListener('click', function() {
// 请求订阅推送
registration.pushManager.subscribe({
userVisibleOnly: true, // 必须设置为 true,表示推送消息始终对用户可见
applicationServerKey: urlBase64ToUint8Array('你的 VAPID 公钥') // 将 VAPID 公钥转换为 Uint8Array
})
.then(function(subscription) {
console.log('订阅成功:', subscription);
// 将订阅信息发送到服务器
fetch('/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'content-type': 'application/json'
}
});
})
.catch(function(error) {
console.error('订阅失败:', error);
});
});
})
.catch(function(error) {
console.error('Service Worker 注册失败:', error);
});
} else {
console.warn('浏览器不支持 Web Push');
}
// 将 URL 安全的 Base64 编码的字符串转换为 Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
</script>
</body>
</html>
3. Service Worker 代码 (service-worker.js)
self.addEventListener('push', function(event) {
console.log('收到推送消息:', event);
const title = '新消息';
const options = {
body: event.data.text(),
icon: '/icon.png',
badge: '/badge.png'
};
event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener('notificationclick', function(event) {
console.log('通知被点击:', event);
event.notification.close();
// 打开网站
event.waitUntil(
clients.openWindow('https://www.example.com')
);
});
4. 后端代码 (server.js)
const express = require('express');
const webpush = require('web-push');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(express.static(__dirname)); // 假设前端文件和 server.js 在同一目录下
// 设置 VAPID 密钥
webpush.setVapidDetails(
'mailto:your-email@example.com', // 你的邮箱地址
'你的 VAPID 公钥',
'你的 VAPID 私钥'
);
// 保存订阅信息的数组(实际应用中应该使用数据库)
const subscriptions = [];
// 订阅推送的路由
app.post('/subscribe', (req, res) => {
const subscription = req.body;
console.log('收到订阅:', subscription);
subscriptions.push(subscription);
res.status(201).json({});
});
// 发送推送消息的路由(这里只是一个示例,你可以根据需要修改)
app.get('/send-notification', (req, res) => {
const payload = JSON.stringify({
title: 'Hello, World!',
body: '这是一条测试消息'
});
// 遍历所有订阅,发送推送消息
subscriptions.forEach(subscription => {
webpush.sendNotification(subscription, payload)
.catch(error => {
console.error('发送推送失败:', error);
});
});
res.status(200).json({ message: '推送消息已发送' });
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器运行在端口 ${port}`);
});
代码解释:
前端部分,首先检查浏览器是否支持serviceWorker
以及PushManager
,注册/service-worker.js
。之后获取用户授权,调用registration.pushManager.subscribe
方法,其中applicationServerKey
需要填写VAPID公钥,并且需要将其转换为Uint8Array
类型。订阅成功后,将订阅信息subscription
发送至服务器。
Service Worker部分,监听push
事件,获取推送消息,然后调用self.registration.showNotification
显示推送通知。之后监听notificationclick
事件,在用户点击通知时打开网站。
后端部分,使用web-push
库配置VAPID密钥。设置/subscribe
路由,获取前端发送的订阅信息,并储存。设置/send-notification
路由,遍历订阅信息,调用webpush.sendNotification
并传入订阅信息以及消息主体payload
来发送推送。
注意:
- 你需要将代码中的
'你的 VAPID 公钥'
、'你的 VAPID 私钥'
和'mailto:your-email@example.com'
替换成你自己的信息。 - 这只是一个简单的示例,实际应用中你需要考虑更多因素,比如错误处理、订阅信息的持久化存储(使用数据库)、更复杂的推送逻辑等。
service-worker.js
文件必须部署在你的网站的根目录下。
总结
VAPID 是 Web 推送中非常重要的一环,它保证了推送消息的安全性和可靠性。通过使用 VAPID,网站可以放心地向用户发送通知,而不用担心被恶意攻击或信息泄露。如果你想在自己的网站上实现 Web 推送功能,VAPID 是你必须掌握的技术。希望这篇文章能帮助你理解 VAPID 的原理和使用方法。现在,去尝试一下吧,让你的网站也能“说话”!