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

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

Markdown 进化论:从小程序文章列表到游戏关卡到时间轴作品集再到项目模块集市

Markdown越来越玩的纯熟,在小程序架构稳定之后,剩下的就是Markdown内容的编辑,我已经不满足于基础的标题和段落,我还会根据适当的场景使用。我必须把常用的Markdown介绍一下,感觉其设计真是奇妙,比如在一行中用三个以上的星号、减号、底线来建立一个分割线,行内不能有其它东西。也可以在星号或减号中间插入空格。这样就是一条分割线。当你完全掌握之后,就会发现我们还可以进行扩展,比如我后面的跳转公众号文章,跳转小程序。 我们先来快速浏览一下除了标题和段落之外不常用但重要的Markdown语法。不感兴趣可以跳过。 *斜体文本* _斜体文本_ **粗体文本** __粗体文本__ ***粗斜体文本*** ___粗斜体文本___ # 分割线 *** * * * ***** - - - --------- ~~删除线~~ <u>下划线文本</u> [^脚注,要注明的文本] # 无序列表 * 第一项 + 第二项 - 第三项 # 有序列表 1. 第一项 2. 第二项 3. 第三项 # 列表嵌套需要在子列表中的选项签名添加两个或四个空格即可。 1. 第一项: - 第一项嵌套的第一个元素 - 第一项嵌套的第二个元素 2. 第二项: - 第二项嵌套的第一个元素 - 第二项嵌套的第二个元素 # 区块 > 最外层 > > 第一层嵌套 > > > 第二层嵌套 # 列表中使用区块,如果列表中要使用区块,请在>前添加四个空格即可。 * 第一项 > 菜鸟教程 > 学的不仅是技术更是梦想 * 第二项 # 链接 [链接名称](链接地址) <链接地址> # 图片显示 ![alt 属性文本](图片地址) ![alt 属性文本](图片地址 "可选标题") ![alt 属性文本](图片地址居中#pic_center) ![alt 属性文本](图片地址宽和高 =400x200) # 只设置宽,会按比例进行缩放 ![alt 属性文本](图片地址 =400x) # 使用 html <img>标签。 <img src="https://xxx.com/x.png" width="50%"> # 制作表格使用|来分割不同的单元格,可以使用-来分割表头和其它行。 | 表头 | 表头 | | ---- | ---- | | 单元格 | 单元格 | | 单元格 | 单元格 | # 设置表格的对齐方式使用如下符号: -: 设置内容和标题栏居右对齐。 :- 设置内容和标题栏居左对齐。 :-: 设置内容和标题栏居中对齐。 # Markdown 使用了很多特殊符号来表示特定的意义,如果需要显示特定的符号则需要使用转义字符,Markdown 使用反斜杠转义特殊字符: **文本加粗** \*\* 正常显示星号 \*\* 使用 KaTex 或 MathJax 来渲染数学表达式。 Mardown语法扩展 当然我还扩展了一些内容,比如链接,我在小程序中支持跳转到公众号文章,以及跳转到小程序。具体的实现就是通过前缀。 ...

2025-09-03 · 1 min · Eagle

万物皆可 Markdown:小程序动态内容渲染通用架构设计

在学习了微信小程序之后,我使用wxml页面可以做出唐诗的内容了。结合wxss,我可以定义好看的布局。在小程序需要备案时,因为唐诗需要资质,而我个人无法满足这些条件,所以我需要开发新的项目。恰好最近在为我的技术笔记发愁。我希望有一个可以展示我笔记内容的工具。 我的最初想法非常简单: 1,可以更新文章内容而不需要升级小程序。 2,需要在小程序端进行展示,并且需要美观。 3,能够根据标题或者关键字进行搜索文章内容。 经过一番研究后,我决定小程序使用原生开发,界面 UI 使用 WeUI,内容渲染使用Markdown。 小程序页面架构是列表+详情。列表作为首页,首页页面非常简单,一个标题和描述,用来解释小程序干什么?一个搜索框用来搜索内容,三个快捷按钮用于快速查找内容。 这个也是为了小程序审核,避免成为资讯类主题。 在完成之后,我发现这套架构可以通用,用来渲染动态内容。像列表页面以及WEUI使用,熟悉前端的非常容易理解实现。下面是一些核心介绍,主要介绍Markdown的渲染实现。 在小程序中,无法直接展示 Markdown 数据,我们需要将 Markdown 转换为 WXML,网上的大神很多,有多款 Markdown 转 WXML 库,我使用的是 TOWXML 库。我使用Markdown主要是因为更新内容不用升级小程序,这是动态渲染的最大优点。当然它还有其它一些优势,比如容易编辑,支持数学公式,布局美观等。 下面是Markdown具体实现,不感兴趣可以跳过。可以看技术下面的架构思考。 1,按照 towxml 库的说明文档,我们将编译后的 towxml 复制到我们的项目中,然后在 app.js 文件中引入这个组件。 App({ towxml: require('./towxml/index'), 2,按照首页的步骤,我们添加文章详情页面。 "pages/article/index" 在 index.wxml 文件中,我们添加页面布局,这个很简单,显示渲染后的内容即可。 <!--pages/article/index.wxml--> <view class="page"> <!--使用towxml--> <towxml nodes="{{article}}" /> </view> 3,注意,我们还需要在 index.json 中添加组件引入。 { "usingComponents": { "towxml": "../../towxml/towxml" } } 4,在 js 文件中,我们需要调用 Markdown 数据。 onLoad(options) { const _ts = this; wx.showLoading({ title: '加载中', }) httpGet('/artd', { uuid: options.guid, }).then((res) => { const result = res.data; if (result.code == 1) { let content = result.data; let obj = app.towxml(content, 'markdown', { theme: 'light', events: { tap: (e) => { console.log('tap', e); } } }); _ts.setData({ article: obj, }); wx.hideLoading({ success: (res) => { }, }) } else { wx.hideLoading() } }).catch((err) => { console.log(err); wx.hideLoading() wx.showToast({ title: '网络异常请重试', }) }) }, 我们在 onLoad 中加载函数,这样页面启动就会自动网络请求,然后渲染 Markdown 数据。在请求数据时,我们注意到 guid,这个参数是上一个页面即首页,点击文章列表项带过来的数据。 你也可以使用其它参数,它能唯一代表从服务器拉取哪篇文章的Markdown内容。 ...

2025-06-03 · 2 min · Eagle

Mosquitto 安全加固从匿名访问到多用户 ACL 细粒度权限控制

我在豆子碎片项目中开始使用MQTT,比如文章上传后我使用了MQTT来通知审核的消息。这样就要求不能让张三接收到属于李四的消息。当然还有其它场景使用MQTT,比如ESP设备连接MQTT。比如用户上传文章后,我的ESP设备的LED就会闪烁,蜂鸣器也会响。我服务器上部署的MQTT Broker是mosquitto。在 Mosquitto 实例上配置身份验证非常重要,这样未经授权的客户端就无法连接。在 Mosquitto 2.0 及更高版本中,你必须明确选择身份验证选项,然后客户端才能连接。早期版本中,默认设置是允许客户端无需身份验证即可连接。 Mosquitto身份验证有三种选择:密码文件、身份验证插件和匿名访问。可以单独使用,也可以使用三个选项的组合。在 Mosquitto2.0 及更高版本中,可以通过配置文件中将 per_listener_settings 设置为 true,让不同的侦听器使用不同的身份验证方法。除了身份验证,还应该考虑访问权限控制,以确定哪些客户端可以访问哪些主题。我们来看下不同选项的配置。 密码文件设置 密码文件是一种将用户名和密码存储在单个文件中的简单机制。适合于有相对较少的静态用户。请注意,修改了密码文件后,需要重新加载 mosquitto。 创建密码文件,可以使用mosquitto_passwd工具,命令如下: mosquitto_passwd -c <password file> <username> 请注意,使用-c 标志将覆盖已存在的文件,如果向已存在文件添加,请去掉-c 标志。 要开始使用密码文件,需要配置 broker,在配置文件中,添加如下项: password_file <path to the configuration file> 密码文件必须能够被运行 mosquitto 的用户读取,也就是必须具备读取它的权限。 身份验证插件设置 如果需要更多的控制来认证用户,则需要使用认证插件。具体的特性依赖于使用的认证插件。 可使用的插件有: mosquitto-go-auth ,它提供了各种后端来存储用户数据,例如 mysql,postgresql,jwt 或 redis 等。 Dynamic security,动态安全插件,仅适用于 2.0 及更高版本,可提供原创管理的灵活的代理客户端、组和角色。 配置身份验证插件依赖你的 Mosquitto 的版本。版本 1.6.x 和以前的版本,使用auth_plugin选项。这个选项在版本 2.0 也被支持。 listener 1883 auth_plugin <path to plugin> 版本 2.0 及更高,使用plugin选项。 listener 1883 plugin <path to plugin> 我使用了这个选项,因为这个可以对接数据库,我可以方便的在数据库中维护权限,这样我不需要像密码文件那样,修改一个权限还需要重新启动Mosquitto。这对于对接其他系统或扩展业务非常方便。 匿名访问设置 配置匿名访问,请使用allow_anonymous选项。 ...

2025-01-01 · 1 min · Eagle

文章上传命令行客户端升级版本使用Cobra支持更多命令

对于添加更多的命令,使用 flag,就有点麻烦了,这次我们使用一个更高级的库 cobra。同时,我们使用 viper 替换 ini 库,这个库可以读取多种格式的配置文件,可以读取环境变量。 要实现的功能如下: 上传文章 文章删除 更新文章标题 更新文章关键字 更新文章内容 文章公开开关 文章加锁开关 根据标题查找文章 根据关键字查询文章 现在使用 cobra 实现上面的命令。 首先,我想要的效果如下: 上传文章 gart upload title keyword filename ispub islock 删除文章 gart remove uuid 更新文章标题 gart updatetitle uuid title 更新文章关键字 gart updatekeyword uuid keyword 更新文章内容 gart updatecontent uuid filename 更新文章公开或不公开 gart updatepub uuid ispub 更新文章加锁或不加锁 gart updatelock uuid islock 根据标题或关键字查找文章 gart search content 上面的 upload,remove,updatetitle,updatekeyword 等,在 cobra 中都是命令。title keyword 等都是参数。 我在读 cobra 文档后,最疑惑的地方就是标志和参数,这两者最大的区别就是标志需要在命令中添加–flag 然后是值,而参数是直接跟在命令后,直接就是内容的。标志可以更改程序的行为。 ...

2024-09-22 · 5 min · Eagle

使用Go自制豆子碎片文章上传命令行客户端

这是 Golang 实现的上传文章以及管理文章的一个命令行工具。 项目地址 https://gitee.com/littletow/upart-go 实现一个上传文章的命令行工具 这是一个最初的版本,使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数,使用 Ini 配置文件,记录识别码,以及 token。记录 token 的原因是因为每次启动命令行,都需要重新获取 token,为了减少 token 获取次数,在获取到 token 后,同时存储到 Ini 配置文件中。每次命令行启动,优先查看配置文件中的 token。 开发这个工具主要有以下几个技术要点: 从命令行获取参数 从配置文件中读取参数 读取文件内容 请求后端接口 我们通过分析后,需要定义以下几个参数: title 标题 keyword 关键字 filename MD 文件名 ispub 是否公开 islock 是否加锁 使用 Go 代码定义参数和结构体 var ( title string keyword string filename string ispub int // 1 pub islock int // 1 lock ) func init() { flag.StringVar(&title, "b", "", "文章题目") flag.StringVar(&keyword, "k", "", "文章关键字") flag.StringVar(&filename, "f", "", "MD文件") flag.IntVar(&islock, "l", 0, "是否加锁") flag.IntVar(&ispub, "p", 0, "是否开放") } func main(){ flag.Parse() } init() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse(),这一步非常关键。之后就可以使用变量了。 ...

2024-09-21 · 3 min · Eagle