29、安全与隐私:HTTPS强制要求、权限管理、媒体流加密
说到WebRTC的安全与隐私,这其实是个绕不开的话题。我刚开始做音视频开发那会儿,总觉得先把功能跑通再说,安全嘛,后面再补。结果呢?有一次在Chrome上调试,直接给我弹了个大红叉——"getUserMedia()只能在安全上下文(HTTPS或localhost)中使用"。嗯,从那以后我就老实了。
说白了,浏览器对音视频权限的管控是越来越严的。这不是坏事,你想想看,如果随便一个网页都能打开你的摄像头和麦克风,那还得了?所以今天我们就来聊聊,怎么在保证功能的同时,把安全和隐私做到位。
29.1 HTTPS强制要求:不是选项,是门槛
WebRTC的核心API——getUserMedia()、RTCPeerConnection——都要求页面运行在安全上下文中。什么是安全上下文?简单说就是:
- HTTPS(正式环境)
- localhost(本地开发)
- file:// 协议(某些浏览器支持,但不推荐)
我在项目中遇到过,有同事把代码部署到HTTP服务器上测试,结果发现摄像头死活打不开。查了半天,控制台报错:getUserMedia() failed: DOMException: Permission denied。其实不是权限问题,是协议不对。
本地开发时,用localhost是最省事的。但如果你需要从局域网其他设备访问(比如手机测试),那就得配HTTPS证书了。我个人习惯用mkcert这个工具,一行命令就能生成受信任的本地证书:
# 安装 mkcert
brew install mkcert # macOS
# 或
choco install mkcert # Windows
# 初始化并生成证书
mkcert -install
mkcert localhost 192.168.1.100 *.example.com
# 启动 HTTPS 服务(以 Node.js 为例)
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('localhost-key.pem'),
cert: fs.readFileSync('localhost.pem')
};
https.createServer(options, app).listen(443);
29.2 权限管理:别一股脑全要
很多开发者写getUserMedia()时,习惯把音频和视频都加上:
// ❌ 不好的做法:一次性请求所有权限
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
这样做的问题是什么?用户一看到弹窗要同时授权摄像头和麦克风,心里就会咯噔一下——"这网站要干嘛?" 尤其是那些只需要语音通话的应用,你请求摄像头权限干嘛?
我建议的做法是:按需请求,分步授权。
另外,别忘了处理权限被拒绝的情况。用户点了"阻止"之后,你总不能一直弹窗吧?
async function requestMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 成功获取,继续处理
return stream;
} catch (err) {
if (err.name === 'NotAllowedError') {
// 用户拒绝了权限
showToast('请允许麦克风权限,否则无法通话');
// 可以引导用户去浏览器设置中手动开启
} else if (err.name === 'NotFoundError') {
// 没有找到麦克风设备
showToast('未检测到麦克风,请检查设备');
}
}
}
这里有个坑:一旦用户点击了"阻止",浏览器会记住这个选择,下次再请求时不会弹窗,直接拒绝。我曾经踩过这个坑,用户说"我明明点了允许啊,怎么还是不行?" 后来才发现,他第一次点的是"阻止",浏览器记住了。
解决办法?可以在页面上提供一个"重新授权"的按钮,引导用户去浏览器设置里手动改回来。或者用navigator.permissions.query()提前检查权限状态:
// 检查摄像头权限状态
const result = await navigator.permissions.query({ name: 'camera' });
if (result.state === 'denied') {
// 权限已被拒绝,引导用户手动开启
showGuide('请在浏览器设置中允许摄像头权限');
} else if (result.state === 'prompt') {
// 尚未授权,可以主动请求
requestCamera();
}
29.3 媒体流加密:DTLS-SRTP 是标配
WebRTC的媒体流加密是默认开启的,用的是DTLS-SRTP协议。DTLS负责密钥协商,SRTP负责数据加密。说白了,你的音视频数据在传输过程中是加密的,别人截获了也看不懂。
但这里有个常见的误解:很多人以为只要用了WebRTC,数据就自动安全了。其实不然,加密只保证传输过程中的安全,不保证端点的安全。比如:
- 如果对方的设备被植入了恶意软件,加密也没用
- 如果信令服务器被攻击,交换的SDP信息可能被篡改
- 如果使用了不安全的STUN/TURN服务器,可能泄露IP地址
下面这张图展示了WebRTC的安全架构:
在实际开发中,你不需要手动配置加密,WebRTC已经帮你做好了。但有几个点需要注意:
- 强制使用加密:在创建
RTCPeerConnection时,可以设置forceEncrypted选项(某些浏览器支持),确保所有媒体流都走加密通道。 - 不要禁用加密:有些老旧的教程会教你设置
googIPv6: false之类的参数,千万别学。这些非标准参数可能会影响安全性。 - 信令通道也要加密:信令服务器建议使用WSS(WebSocket Secure),否则SDP信息可能被中间人篡改。
29.4 隐私保护:不只是加密
除了加密,隐私保护也是个大话题。WebRTC有个著名的"漏洞"——IP地址泄露。即使你用了VPN,WebRTC的ICE协商过程中可能会暴露你的真实IP。
为什么会这样?因为浏览器在收集ICE候选时,会收集所有网络接口的IP地址,包括内网IP和公网IP。即使你用了STUN/TURN服务器,浏览器也可能直接发送本地IP。
解决办法?
- 使用TURN服务器:强制所有流量走TURN中继,不直接P2P连接。这样对方的IP就不会暴露。
- 设置ICE传输策略:在创建
RTCPeerConnection时,可以设置iceTransportPolicy: 'relay',强制只使用中继候选。 - 过滤本地IP:在
onicecandidate回调中,可以过滤掉包含内网IP的候选。
// 强制只使用 TURN 中继
const pc = new RTCPeerConnection({
iceTransportPolicy: 'relay',
iceServers: [
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// 或者在回调中过滤
pc.onicecandidate = (event) => {
if (event.candidate) {
// 过滤掉包含内网IP的候选
if (event.candidate.candidate.includes('192.168.') ||
event.candidate.candidate.includes('10.')) {
return; // 不发送这个候选
}
}
// 发送其他候选
};
我个人建议,如果应用场景不需要P2P(比如多人会议),直接用TURN中继是最省心的。虽然会增加服务器带宽成本,但隐私保护更到位。
29.5 实战建议:安全清单
最后,我整理了一份安全清单,你可以对照检查自己的应用:
| 检查项 | 要求 | 说明 |
|---|---|---|
| HTTPS | 必须 | 所有页面和API都走HTTPS |
| 权限请求 | 按需 | 不要一次性请求所有权限 |
| 权限拒绝处理 | 必须 | 引导用户手动开启,不要反复弹窗 |
| DTLS-SRTP | 默认开启 | 不要手动禁用 |
| 信令通道 | WSS | WebSocket Secure,防止SDP被篡改 |
| IP泄露防护 | 建议 | 使用TURN中继或过滤本地IP |
| 设备标识 | 谨慎 | 不要滥用deviceId,可能被用于追踪 |
嗯,安全这块其实还有很多细节,但上面这些是WebRTC开发中最常见的坑。记住一句话:安全不是功能,而是基础。没有安全,功能再强大也没用。
好了,这一章就到这里。希望你在实际项目中,能把安全和隐私放在第一位。