27、安全与隐私:DTLS-SRTP加密、权限管理、安全最佳实践
做WebRTC开发这么多年,我越来越觉得安全不是锦上添花,而是地基。你想想看,音视频数据一旦被窃听或篡改,后果有多严重?我早期做的一个项目,就因为忽略了DTLS握手超时配置,导致用户频繁断连,排查了整整两天才找到根儿。今天咱们就把安全这块彻底聊透。
DTLS-SRTP:你的音视频数据在裸奔吗?
WebRTC默认使用DTLS-SRTP来加密媒体流。说白了,DTLS负责握手和密钥交换,SRTP负责用这些密钥加密实际的音视频包。两者配合,确保你的通话内容不会被中间人偷听。
核心流程:
- DTLS握手阶段:双方交换证书,协商加密套件
- 导出SRTP密钥:握手完成后,DTLS层导出密钥材料
- SRTP加密:后续所有RTP/RTCP包都用这个密钥加密
我在项目中遇到过一个问题:某些老旧浏览器不支持DTLS 1.2,导致握手失败。解决办法是降级到DTLS 1.0,但安全性会打折扣。嗯,这里要注意,如果你的用户群体偏技术向,建议强制要求DTLS 1.2以上。
DTLS-SRTP的配置细节
实际开发中,你需要在SDP中明确指定加密方式。看下面这个例子:
// 创建PeerConnection时启用DTLS-SRTP
const config = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
sdpSemantics: 'unified-plan',
// 强制使用DTLS-SRTP
certificates: [await RTCPeerConnection.generateCertificate({
name: 'ECDSA',
namedCurve: 'P-256'
})]
};
const pc = new RTCPeerConnection(config);
我个人习惯用ECDSA证书而不是RSA,因为它的密钥更短,握手速度更快。你想想看,在移动端网络不稳定的情况下,每次握手少几十毫秒,体验差距就出来了。
避坑指南:我曾经在生产环境中发现,某些自签名证书会导致DTLS握手失败。后来统一改用浏览器生成的证书(如上例),问题就解决了。记住,不要手动生成证书,让浏览器自己搞定。
权限管理:别让摄像头随便被打开
WebRTC的权限管理主要涉及摄像头、麦克风和屏幕共享。浏览器会弹出权限请求框,但作为开发者,你还需要做更多。
我见过最糟糕的做法:页面一加载就请求摄像头权限。用户还没搞明白怎么回事,就被要求授权,结果直接拒绝。正确的做法是:
- 延迟请求:等到用户点击"开始通话"按钮时再请求权限
- 明确说明用途:告诉用户为什么需要摄像头,比如"为了视频通话"
- 处理拒绝情况:用户拒绝后,优雅降级到纯音频或文字聊天
// 优雅处理权限拒绝
async function startVideoCall() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 正常通话
} catch (err) {
if (err.name === 'NotAllowedError') {
// 用户拒绝了权限
console.warn('用户拒绝了摄像头/麦克风权限');
// 降级到纯音频
startAudioOnlyCall();
} else if (err.name === 'NotFoundError') {
console.warn('没有找到摄像头设备');
// 提示用户连接设备
}
}
}
注意:在iOS Safari上,getUserMedia必须在用户手势事件(如click)中调用,否则会静默失败。我踩过这个坑,排查了半天才发现是iOS的隐私策略。
安全最佳实践:从实战中总结的6条铁律
做安全没有银弹,但有一些经过验证的实践可以大幅降低风险。我根据自己的项目经验,整理了下面这些要点:
| 实践 | 说明 | 我的经验 |
|---|---|---|
| 强制HTTPS | WebRTC要求页面必须通过HTTPS加载 | 本地开发可以用localhost,但生产环境必须HTTPS |
| 证书验证 | DTLS握手时验证对端证书指纹 | 我曾经遇到过中间人攻击,就是因为跳过了证书验证 |
| 密钥更新 | 定期更新SRTP密钥,防止长期使用同一密钥 | 长会议场景下,建议每30分钟重新协商一次密钥 |
| ICE候选者过滤 | 只允许可信的ICE候选者 | 我在做企业级应用时,会白名单内网IP段 |
| 日志脱敏 | 不要在日志中打印SDP或ICE候选者 | 有一次debug时不小心把SDP打到了日志里,赶紧清理了 |
| 超时控制 | DTLS握手设置合理的超时时间 | 默认30秒太长,我一般设为10秒 |
DTLS-SRTP的完整握手流程
为了让你更直观地理解整个过程,我画了一张流程图。这张图展示了从建立连接开始,到媒体数据加密传输的完整链路。
这张图里最关键的是第⑤步——密钥导出。DTLS握手完成后,双方会协商出一个主密钥,然后通过密钥派生函数生成SRTP需要的加密密钥和盐值。这个过程是自动的,你不需要手动干预。
证书指纹验证:防止中间人攻击的最后一道防线
默认情况下,WebRTC的DTLS握手会验证证书链,但如果你用的是自签名证书(很多场景下都是),就需要手动验证证书指纹。
我曾经在一个金融类项目中,要求双方在带外交换证书指纹。具体做法是:
// 获取本地证书指纹
const certs = await RTCPeerConnection.getCertificates();
const localFingerprint = certs[0].getFingerprint();
// 通过信令通道发送给对端
signaling.send({ type: 'fingerprint', value: localFingerprint });
// 收到对端指纹后,在连接建立时验证
pc.addEventListener('connectionstatechange', () => {
if (pc.connectionState === 'connected') {
const remoteFingerprint = pc.getRemoteCertificates()[0].getFingerprint();
if (remoteFingerprint !== expectedFingerprint) {
console.error('证书指纹不匹配,可能存在中间人攻击!');
pc.close();
}
}
});
重要提醒:证书指纹验证必须在带外进行,也就是通过你的信令服务器传递,而不是通过WebRTC的数据通道。否则攻击者可以同时篡改信令和媒体通道,验证就失去了意义。
权限管理的进阶技巧
除了基本的getUserMedia权限,还有一些容易被忽视的权限问题:
- 屏幕共享权限:getDisplayMedia会弹出屏幕选择对话框,用户可以选择分享整个屏幕、某个窗口或浏览器标签页。注意,你不能强制用户分享特定内容。
- 音频输出设备:setSinkId可以控制音频输出到哪个设备(如扬声器或耳机),但需要用户授权。
- 媒体流暂停:用户可以通过浏览器自带的控件暂停摄像头或麦克风,你的应用需要监听这些事件并做出响应。
// 监听用户通过浏览器控件暂停媒体流
stream.getVideoTracks()[0].addEventListener('ended', () => {
console.warn('用户关闭了摄像头');
// 显示占位头像或文字提示
showPlaceholderAvatar();
});
// 监听音频输出设备变化
audioElement.addEventListener('sinkchange', () => {
console.log('音频输出设备已切换');
});
嗯,这里要注意,不同浏览器对权限的处理方式略有差异。比如Firefox允许用户单独禁用摄像头但保留麦克风,而Chrome则是整体拒绝。我在做跨浏览器兼容时,会针对不同浏览器写不同的降级逻辑。
总结一下
安全不是一蹴而就的,它需要贯穿整个开发周期。从DTLS-SRTP的配置,到权限管理的细节,再到证书验证的严谨,每一步都马虎不得。我个人习惯在项目初期就把安全方案定下来,而不是等到上线前再补。你想想看,一旦用户数据泄露,再多的功能优化也挽回不了信任。
最后送大家一句话:安全不是功能,而是态度。希望今天的分享能帮你少踩一些坑。
核心要点回顾:
- DTLS-SRTP是WebRTC的默认加密方案,必须正确配置
- 权限管理要延迟请求、明确用途、优雅降级
- 证书指纹验证是防止中间人攻击的关键
- 安全实践要贯穿开发始终,不要事后补救