在开发“模块集市”时,用户通过观看激励广告获取源码下载地址。虽然对接广告并不难,但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调(Server-to-Server),奖励的发放完全依赖前端触发,这给了一些“羊毛党”可乘之机。
在接口防护的过程中,我经历了四个阶段:
第一阶段:身份鉴权(解决匿名刷取)
我的获取奖励接口是getAdReward。调用它就可以获取奖励,后台进行发放。最初,由于经验不足,我没有对它做任何防护。这样任何人任何客户端都可以进行访问。了解小程序微信账号体系后,我添加了鉴权,只有携带Token的用户可以访问。这样可以保证只有能够调用wx.login的用户才可以访问。
这解决了一部分问题,但是又有了新的问题,它防不住“真实用户”,只要用户登录后拦截并解析出接口地址和 Token,就可以跳过广告手动调用了getAdReward接口,没有观看广告就获取了奖励。
第二阶段:共享密钥加密(解决接口暴露)
为了防止直接调用接口,我引入了对称加密。前端与后端约定一个硬编码的共享密钥,在小程序端和服务端同时使用这个共享密钥加密随机数和时间戳进行校验,匹配则发放奖励。这样可以防止fidder等工具直接拦截调用接口。
它正常运行了一段时间,我发现又有了新的情况。用户可能通过反编译或者其它方式获取到了共享密钥。这在一段时间内对我造成困扰,除了定期更新共享密钥,我没有好的办法,但更新共享密钥需要升级发布小程序。直到当我了解到小程序提供加密网络通道时,我发现这是一个好的解决方案。
第三阶段:微信加密网络通道(解决密钥安全)
首先,我们来了解一下加密网络通道,它是微信为小程序提供的一套加密KEY机制,它可以同时在微信小程序端和服务器端获取相同的密钥。由于密钥由微信官方通道生成且动态更新,反编译源码也拿不到密钥。除非攻击者能拦截微信的网络通道,否则无法伪造解密过程。同时,配合时间戳校验,有效防御了初级的重放攻击。这对于我来说,是非常好的利好消息,我不需要在源码中硬编码共享密钥了。
我们可以参考获取短信验证码,短信验证码之所以安全,是因为应用和短信验证码是两个不相同的通道。同样的这个加密KEY,在小程序获取和服务端获取都是通过微信的网络通道,和自己的应用通道也不是一个。对于我的小程序稍加改造就可以了。在获取奖励的接口中,首先使用wx.getUserEncryptKey获取微信的加密KEY,然后使用自定义的AES加密方法,将要传递的时间戳和Nonce等参数加密后,将加密数据提交给服务器就可以了。当数据到达服务器,服务器同样调用微信的接口获取微信的加密KEY,然后进行AES解密。这样就可以防止获取共享密钥的弊端。
好了,我们来看看具体如何使用?为了避免小程序与开发者后台通信时数据被截取和篡改,微信侧维护了一个用户维度的可靠key,用于小程序和后台通信时进行加密和签名。开发者可以分别通过小程序前端和微信后台提供的接口,获取用户的加密 key。
1,小程序端获取:
const somedata = 'xxxxx'
const userCryptoManager = wx.getUserCryptoManager()
userCryptoManager.getLatestUserKey({
success({encryptKey, iv, version, expireTime}) {
const encryptedData = someAESEncryptMethod(encryptKey, iv, somedata)
wx.request({
data: encryptedData,
success(res) {
const decryptedData = someAESDEcryptMethod(encryptKey, iv, res.data)
console.log(decryptedData)
}
})
}
})
其中,someAESEncryptMethod 和 someAESDEcryptMethod 分别为加解密函数,由开发者自行引入加解密库来实现,基础库暂时不提供加解密能力。
2,服务端获取:
在开发者服务端,可以调用getUserEncryptKey后台接口获取用户最近三次的key。在获取key的同时,接口会携带version信息,开发者可以比较version版本来选择使用对应的key对数据进行加解密。
curl -X POST "https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=ACCESS_TOKEN&openid=OPENID&signature=SIGNATURE&sig_method=hmac_sha256"
其中,openid是用户的openid,signature用sessionkey对空字符串签名得到的结果。即 signature = hmac_sha256(session_key, “"),sig_method为签名方法,固定为hmac_sha256。
这个是核心,获取激励广告奖励基于它再添加一些参数进行加解密。
第四阶段:后端“挑战模式”(解决重放与逻辑伪造)
我在参数中添加的Nonce,它是一个噪音参数,它是小程序端本地生成的,还有时间戳也是本地生成的,增加它们是为了增加解密难度。在实际运行中,我发现硬核玩家会通过修改本地时间戳和 Nonce(随机数)来绕过检测,或者使用相同的参数进行重放攻击,或者不够15s(广告正常播放最短时间)调用多次接口。
为了解决这个问题,我将逻辑升级为“挑战模式”:首先,小程序端在广告开始前进行一次预请求,前端必须先调用 getNonce 接口获取Nonce然后使用这个Nonce进行加密,getNonce是一个新增接口,用来生成Nonce,并记录生成时间和关联openid,服务端存入缓存(如 Redis),完成服务端打标。当前端再次调用getAdReward接口时,如果差值小于广告常规时长(如 15s),则判定为非正常观看,直接拒绝。
通过这种“加密网络通道 + 后端挑战模式”的组合拳,我成功将防刷成本提高到了用户收益之上。虽然世上没有绝对的安全,但当破解成本远超收益价值时,系统就是安全的。
这就是我的广告防刷经历,后续我会将这套小程序加密网络通道模块的源码整理上线到“模块集市”,希望能帮到有同样困扰的开发者。