小程序激励广告防刷演进史:从前端校验到“挑战模式”

在开发“模块集市”时,用户通过观看激励广告获取源码下载地址。虽然对接广告并不难,但如何防刷成了我最头疼的问题。由于小程序激励广告缺乏服务器回调(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),则判定为非正常观看,直接拒绝。 ...

2026-04-07 · 1 min · Eagle

我是如何设计和实现模块源码集市的?

当我开发完51单片机温度采集并转换Modbus RTU后,我一度想分享出去,但是我又不想草率的去分享。我一直在思考,如何能让这些代码发挥更大的价值,而不仅仅是躺在个人仓库里。作为一名摸爬滚打多年的开发者,我手头积累了大量来自实践的代码、模块和解决方案。它们散落在硬盘的各个角落,像一颗颗未经打磨的珍珠。 于是,我萌生了做一个“模块源码集市”小程序的想法——一个可以直接浏览、查阅、并一键下载优质源码的平台。 经过最近一段时间的集中开发,这个小程序终于和大家见面了。今天,我想和大家分享其背后的设计与思考,希望能为有相似想法的朋友提供一些参考。这个小程序的核心功能简洁而实用: 首页全景浏览:所有上架的代码模块都在首页清晰展示,你可以快速了解其名称、简介和分类,对全站资源一目了然。 沉浸式详情阅读:点击任一模块,即可进入详情页。这里我使用了Markdown渲染引擎,来展示模块的详细介绍、使用说明、技术要点等。Markdown的优雅排版能让技术文档的阅读体验大幅提升。 一键获取源码:在详情页,点击获取模块源码链接按钮,即可轻松获取模块的完整源码压缩包。 为了让这个简单的流程稳定、安全、可扩展,我在几个关键环节做了一些设计和取舍: 动态资源与安全通道源码文件并非直接打包在小程序内,而是存储在后台的MinIO对象存储中。当用户点击下载时,服务端会动态生成一个具有时效性的访问链接返回给小程序。这样做的好处是资源可以随时更新。同时,为了维持后端服务器持续运行,我在列表及详情中接入了少量广告。为了保护下载接口不被恶意刷取,我集成了激励式视频广告。用户观看一则简短的广告,即可解锁下载权限。这不仅是简单的广告接入,更重要的是,我为此构建了一套防刷机制,我使用了微信的加密网络通道,对关键数据进行加密传输和验证,极大地增加了自动化盗刷的成本和难度,保护了服务器资源。 灵活的沟通与通知我深知,一个“活”的产品需要与用户保持沟通。因此,小程序内设置了系统消息中心,我可以在这里向所有用户下发重要的系统公告、模块上新以及更新日志。更重要的是,我为每个模块的上新功能,接入了微信服务通知。当有新的、优秀的代码模块入库时,订阅了的用户会在早上8点后收到一条微信消息提醒,能让你随时跟上“仓库”的更新节奏。 持续生长的内容生态目前上架的或正在上架的模块,覆盖了我擅长的多个领域:小程序、Go语言后端工具、Rust系统编程实践、Web前端片段、嵌入式软硬件代码,包含音视频、网络、加密等技术要点,形式包括客户端、命令行工具、固件、小程序、网页等。这只是一个开始。我的计划是将其作为一个长期项目,持续将我实践中验证过的、有价值的代码沉淀、整理并开源出来。未来,我还会在项目详情页中,增加关联的公众号技术文章跳转,或是有功能关联的其他小程序跳转,形成一个立体化的技术知识网络。 坦率地说,这个小程序的开发过程远超我最初的预期。从架构设计到具体实现,尤其是几个关键的技术卡点上,耗费了我大量的精力。 与广告盗刷的攻防:我的服务器每天都有人在扫描,所以在实现激励广告防刷机制时,我花了大量时间加强防刷机制,其实这个在微信小程序社区中也可以看到很多类似的问题。我这里研究了一种解决方案。就是使用加密网络通道。在对接的过程中,我与各种加密错误、签名错误(如常见的40079错误)作斗争。例如,处理会话密钥时,发现不需要进行Base64编码,而是直接以空字符串的字节形式处理;在URL拼接的细节上,POST与GET方法的误用也会导致失败;而加解密环节更是陷阱重重——加密密钥需要从Base64解码,IV却是一个直接的16位字符串,加密数据则需要从十六进制格式解码……这些细枝末节,任何一个出错都会导致整个流程崩掉,往往需要清空Token重新登录来排查。这个过程让我对网络数据安全有了更深刻的认识。这些错误和经验本想专开一篇文章介绍,但是细想一下,可能一段话就能总结,但是其中的辛酸和感悟无法表达在纸面上。索性后期整理出来开源这个模块,减少其他开发者的一些坎坷。 存储方案的抉择:在对象存储的选择上,也有一段小插曲。我最熟悉的是Mino,但是在我了解到MinIO版本已归档,便尝试了rustfs等其他方案进行探索。但经过综合对比在可靠性、API友好度以及与现有技术栈的契合度后,最终还是选择了功能完备、文档清晰的MinIO作为存储后端,事实证明这个选择是稳定高效的。 所幸,在这个艰难的过程中,AI编程助手给予了我巨大的帮助(但是也让我吃了很多苦头,因为它上面的资料都比较旧)。许多代码片段的生成、调试过程中错误信息的解读、API文档的快速查询,都因AI的介入而效率倍增。如果没有它,单靠我自己,恐怕几个月也难见成果,绝无可能在十几天内推动项目成型上线。 做这个小程序,并非为了追逐热点,这个小程序刚刚上线,还需要时间去细细打磨。开发其初心很简单:整理自己的知识资产,并以一种对开发者最友好、最便捷的方式分享出去。我希望它不仅仅是一个下载站点,更可以成为一个高质量的、经过实战检验的“代码片段集市”和“工具箱”。路漫漫其修远兮。在代码世界的探索中,每个人都是学生,也是老师。我在这里抛砖引玉,期待这些代码模块能真正帮助到在具体问题上寻找解决方案的你。也欢迎你常来看看,这些项目模块,会和我一样,在技术的道路上不断生长。 扫码体验:

2026-03-26 · 1 min · Eagle

基于 Go + 小程序实现网页端“扫码登录”实战

不想使用账号和密码登录,害怕被攻击,也不想做注册等功能;不想使用手机号和验证码登录,没有钱也没有资质去做这些。我想到了二维码,通过二维码进行登录。这里有一个核心的问题还是如何鉴权? 在了解小程序后,我就决定使用小程序扫描二维码登录,使用小程序自带的微信账户体系完成鉴权。 我们要知道,二维码(QR Code)是连接物理世界与数字世界的“虫洞”。在登录系统中,它承载着一个临时的身份信标。 在实现这个Web登录功能的过程中,我们需要: 1. 生成有效二维码 这个二维码需要显示在网页上,供用户扫码登录,登录二维码通常包含一个加密的 URL 或一个唯一的 UUID(通用唯一识别码)。 这个唯一识别码需要具备的特性: 唯一性: 每一对扫描动作都必须对应一个独一无二的 ID。 时效性: 二维码必须配合 Redis 设置过期时间(如 2 分钟),逾期自动失效。 在本项目中,我们将用户的微信 OpenID 作为 Key,生成的验证码作为 Value,使用Redis进行存储: // 存储验证码,并设置 5 分钟过期 err := rdb.Set(ctx, "user:123:code", "8888", 5*time.Minute).Err() // 读取验证码 val, err := rdb.Get(ctx, "user:123:code").Result() 当有了二维码内容后,我们需要工具来生成二维码,在 Go 生态中,我们使用 skip2/go-qrcode 库来完成像素的绘制: // 生成二维码字节数组 var png []byte png, _ = qrcode.Encode("91demo.top"+sessionID, qrcode.Medium, 256) 为了防止用户伪造扫码请求,二维码里的内容通常是加密的或者是不可预测的长随机数(UUID)。只有真实存在的 ID 才能通过后端的 Redis 校验。 2. 传输二维码 当在服务端生成二维码后,还需要传递给浏览器,让浏览器进行显示,用户才可以扫码。这里有两个方法:1,生成图片文件,然后浏览器下载后显示。2,将图片内容转为Base64字符串传递给浏览器。这里我们选择了后者,我们不希望在用户硬盘上产生大量的临时 .png 文件。 在Go中可以这样操作: // 转换为前端可直接识别的 Data URL base64Img := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) 为了让前端不至于崩溃,后端必须返回统一的格式。 func HandleLogin(c *gin.Context) { // 逻辑处理... c.JSON(200, gin.H{ "code": 1, "content": base64Img, }) } 在前端,我们不再需要引入沉重的第三方库来做简单的请求。浏览器原生提供的 Fetch API 简洁且基于 Promise。 ...

2025-03-15 · 2 min · Eagle