第三章 小程序网络通信安全:HTTPS配置、证书绑定、请求签名、防重放攻击
说实话,小程序网络通信这块,是我见过最多人踩坑的地方。
很多开发者觉得,只要用了 HTTPS 就万事大吉了。嗯,我以前也这么想。直到有一次,我帮一个金融类小程序做安全审计,发现他们的请求包居然能被完整抓包解析——虽然用了 HTTPS,但证书校验根本没做对。
今天我们就来把这几个关键点彻底讲透。
3.1 HTTPS 配置:不只是打个勾那么简单
小程序要求所有请求必须走 HTTPS,这个大家都知道。但配置 HTTPS 时,有几个细节我建议你特别注意。
核心要点:小程序后台的「合法域名」配置,必须和证书上的域名完全一致。包括 www 前缀。
我在项目中遇到过一个问题:证书配的是 api.example.com,但小程序请求写的是 example.com/api。结果呢?请求直接报错。你想想看,浏览器访问可能还能自动跳转,但小程序不会给你任何容错。
配置步骤其实不复杂:
- 在微信公众平台「开发」-「开发设置」中配置服务器域名
- 确保 HTTPS 证书是正规 CA 签发的(别用自签名证书)
- TLS 版本必须支持 1.2 及以上
警告:千万不要为了省事把「不校验合法域名」勾上。那是开发调试用的,上线前必须关掉。我曾经见过一个线上事故,就是因为这个选项没关,导致用户数据被中间人截获。
3.2 证书绑定:给通信加把锁
HTTPS 本身是安全的,但有个问题——如果用户的设备被植入了恶意根证书,HTTPS 也能被解密。这就是所谓的「中间人攻击」。
怎么防?证书绑定(Certificate Pinning)。
说白了,就是客户端只认你指定的那个证书。别的证书就算看起来合法,也一律拒绝。
在小程序里实现证书绑定,我一般用这个思路:
// 获取服务器证书的公钥哈希值
// 在请求回调中校验
const checkCertificate = (response) => {
// 这里假设 response 里包含了证书信息
const serverCertHash = response.certificateHash;
const pinnedHash = '你的预置哈希值';
if (serverCertHash !== pinnedHash) {
throw new Error('证书校验失败,可能存在中间人攻击');
}
};
// 使用时
wx.request({
url: 'https://api.example.com/data',
success: (res) => {
try {
checkCertificate(res);
// 正常处理数据
} catch (e) {
// 处理异常
}
}
});
提示:证书绑定不要只绑一个证书。建议绑定证书的「公钥」而不是整个证书。这样证书续期时,只要公钥不变,就不会影响业务。我习惯绑定 2-3 个备用公钥,防止主证书出问题。
3.3 请求签名:防止数据被篡改
HTTPS 保证了传输过程的安全,但到了服务器端,你怎么确认这个请求真的是你的小程序发出来的?
答案就是请求签名。
我常用的签名方案是这样的:
- 客户端把请求参数按字典序排序
- 拼接成字符串,加上时间戳和随机数
- 用约定的密钥进行 HMAC-SHA256 加密
- 把签名放在请求头里传给服务器
来看个具体例子:
// 生成签名
const generateSignature = (params, secretKey) => {
// 1. 排序参数
const keys = Object.keys(params).sort();
let signStr = '';
// 2. 拼接
for (let key of keys) {
signStr += `${key}=${params[key]}&`;
}
// 3. 加上时间戳和随机数
const timestamp = Date.now();
const nonce = Math.random().toString(36).substr(2, 10);
signStr += `timestamp=${timestamp}&nonce=${nonce}`;
// 4. HMAC 加密
const signature = CryptoJS.HmacSHA256(signStr, secretKey).toString();
return {
signature,
timestamp,
nonce
};
};
// 使用
const params = {
userId: '12345',
amount: 100,
type: 'pay'
};
const signedData = generateSignature(params, '你的密钥');
wx.request({
url: 'https://api.example.com/pay',
header: {
'X-Signature': signedData.signature,
'X-Timestamp': signedData.timestamp,
'X-Nonce': signedData.nonce
},
data: params,
success: (res) => {
// 处理结果
}
});
关键点:密钥绝对不能写死在代码里。我建议通过登录接口动态获取,或者使用小程序的云函数来管理密钥。曾经有个项目,密钥写死在 JS 里,结果被反编译直接拿到了——那叫一个惨。
3.4 防重放攻击:让过期请求失效
有了签名,攻击者就不能篡改数据了。但他可以「重放」——把同一个请求再发一次。
比如一个转账请求,如果被重放,用户可能被扣两次钱。这就是重放攻击。
防重放的核心思路就两个:
- 时间戳校验:服务器检查请求时间戳,超过 5 分钟的直接拒绝
- 随机数去重:每个请求带一个唯一随机数,服务器记录已用过的随机数
我一般这样实现:
// 服务器端伪代码
const checkReplayAttack = (timestamp, nonce, signature) => {
// 1. 检查时间戳
const now = Date.now();
if (now - timestamp > 5 * 60 * 1000) {
return false; // 请求过期
}
// 2. 检查随机数是否已使用
if (redis.exists(`nonce:${nonce}`)) {
return false; // 重复请求
}
// 3. 记录随机数(设置过期时间,防止内存泄漏)
redis.setex(`nonce:${nonce}`, 300, '1');
// 4. 验证签名(略)
return true;
};
避坑指南:我曾经犯过一个错——随机数只用时间戳。结果同一毫秒内的请求全被当成重放。后来改成「时间戳 + 随机字符串」的组合,才彻底解决。记住,随机数必须保证全局唯一。
3.5 把这些组合起来:一个完整的请求流程
说了这么多,我们来串一下完整的流程:
| 步骤 | 客户端 | 服务器 |
|---|---|---|
| 1 | 生成请求参数 | 等待请求 |
| 2 | 计算签名(含时间戳+随机数) | - |
| 3 | 通过 HTTPS 发送请求 | 接收请求 |
| 4 | - | 校验证书绑定 |
| 5 | - | 检查时间戳是否过期 |
| 6 | - | 检查随机数是否重复 |
| 7 | - | 验证签名 |
| 8 | 收到响应 | 返回数据 |
你看,每一步都在解决一个特定的安全问题。少了任何一环,整个安全体系就会出现漏洞。
最后提醒:安全不是一劳永逸的事。我建议每季度做一次安全审计,检查证书是否过期、签名算法是否需要升级、随机数存储是否正常。别等到出了事故才想起来——那代价可就大了。
好了,网络通信安全这块就讲到这里。下一章我们会聊聊小程序本地存储的安全问题,包括数据加密、Key 管理、以及如何防止数据被篡改。到时候见。