从 PHP 到 AI + Golang,程序员自救转型手记(十八):点选验证码初版、封装随机数与文件系统包

发布时间:2026/7/2 20:20:15

从 PHP 到 AI + Golang,程序员自救转型手记(十八):点选验证码初版、封装随机数与文件系统包 这是一个系列教程作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-mall 项目的制作。在上一期我们已经完成 “登录接口完善登录页接口整合解决跨域”本期将完成点选验证码初版、封装随机数与文件系统包点选验证码实现设计数据模型模型规划表名captcha用于存放点选验证码数据也可以存放其他 OTP一次性密码场景的验证数据比如邮箱验证码、短信验证码只需要多加一个 code 字段即可字段名备注key主键验证码查询键验证码唯一身份令牌提交验证时携带code验证码值加密后的点选验证码不一定使用info验证码详细数据点选验证码是存储元素的坐标 文字 JSON 图片宽高等所有验证图片信息用于验证点选坐标正确性expired_at过期时间created_at创建时间不需要以下字段status 验证状态验证成功应直接删除使之无法再次使用特殊情况如仅用于展示能否验证成功的预检可不删除实际业务中只要进入验证成功逻辑应立即删除还可以顺便防止重复提交bg_img 背景图片不需要我们会提前准备 N 张大小相同的背景图总是随机显示即背景图不应影响验证的操作或结果让 cc 根据以上规划于internal\model\common.go创建Captcha模型这也是Token模型所在的文件我们的规划中单文件可以存放多个模型且有关联的模型/有相同分类的模型建议放在一起。编译通过无错误。Captcha 模型已创建完成总结如下 ┌───────────┬──────────────┬──────────────────────────────────────────────────────────┐ │ 字段 │ 类型 │ 说明 │ ├───────────┼──────────────┼──────────────────────────────────────────────────────────┤ │ Key │ varchar(64)│ 主键验证码唯一身份令牌请求方携带此键验证 │ ├───────────┼──────────────┼──────────────────────────────────────────────────────────┤ │ Code │ varchar(255)│ 加密后的验证码值json:-不序列化到响应中 │ ├───────────┼──────────────┼──────────────────────────────────────────────────────────┤ │ Info │ text │ 验证元素坐标 文字 JSON 图片宽高json:-不返回前端 │ ├───────────┼──────────────┼──────────────────────────────────────────────────────────┤ │ ExpiredAt │ timestamp │ 过期时间建索引加速过期清理 │ ├───────────┼──────────────┼──────────────────────────────────────────────────────────┤ │ CreatedAt │ timestamp │ 创建时间 │ └───────────┴──────────────┴──────────────────────────────────────────────────────────┘ - Code 和 Info 均使用 json:-防止敏感数据泄露到 API 响应中 - 已在 init()中注册到 RegisterAutoMigrate 会自动建表表名 captchas写模型其实自己也能写但是我偏偏是写了表格然后让 AI 再过一遍果然 AI 还是很灵性的给ExpiredAt加了索引未来也确实需要定时清理过期验证码。然后是受益于我们以前的超前规划新的模型只需要调用注册函数注册即可自动迁移建表funcinit(){// 注册自动迁移Register(Token{},Captcha{})}// Token 令牌模型用于存储各类用户令牌typeTokenstruct{Tokenstringgorm:comment:令牌;type:varchar(64);primaryKey json:-Typestringgorm:comment:令牌类型;type:varchar(32);not null json:typeUserIDuintgorm:comment:用户ID;not null;index json:user_idCreatedAt time.Timegorm:comment:创建时间 json:created_atExpiredAt time.Timegorm:comment:过期时间;not null;index json:expired_at}// TableName 指定表名func(Token)TableName()string{returntokens}// Captcha 验证码模型typeCaptchastruct{Keystringgorm:comment:验证码查询键;type:varchar(64);primaryKey json:keyCodestringgorm:comment:验证码值加密后;type:varchar(255) json:-Infostringgorm:comment:验证码数据坐标文字JSON图片宽高等;type:text json:-ExpiredAt time.Timegorm:comment:过期时间;not null;index json:expired_atCreatedAt time.Timegorm:comment:创建时间 json:created_at}// TableName 指定表名func(Captcha)TableName()string{returncaptchas}而且我们的模型注册函数还设计为支持任意数量的参数此时刚好用上。点选验证码实现初版点选验证码注定不是一个简单的功能哪怕结合 AI也需要仔细把关和整理而且验证码必然涉及文件操作和随机数生成我们在开发验证码期间应该会将随机数生成和文件操作提取为单独的公共包。以下是首版提示词这类功能一般一次性写不出来或者写出来也不能用那就直接将需求写细一点先找找感觉新增验证码配置文件config/captcha.yaml提供元素、长度、混淆点长度、验证码默认过期时间配置其中元素的值类型为数组可选值为中文文字、英文大写字母、ICON三种可混合配置长度和混淆点长度默认均为2默认过期时间为 600 秒我已于asset\captcha\click准备好了点选验证码背景图和ICON图于asset\font\SourceHanSansCN-Normal.ttf准备好了思源黑体字体简体中文字体文件供你后续使用建立internal\infra\captcha包实现点选验证码生成功能有以下要求建立 init 函数其中完成过期验证码清理验证元素Icon的文件名即为它的名称中文元素请生成随机字符应用验证码配置文件中的配置config/captcha.yaml至少应该有创建点选验证码、检查点选验证码公开两个方法创建点选验证码先随机取一张背景图然后根据配置随机出 ICON、中文文字、英文大写字母并随机绘制到背景图片上不能绘制到背景图以外然后将绘制坐标存入验证码数据库Captcha模型最终需要返回验证图片高宽一般就是背景图的高宽验证元素数组原文即 ICON 的名称随机的中文或英文大写字母验证图片的 base64不需要存储图片而是直接输出 base64验证码 key检查点选验证码根据key和 用户点击出的元素顺序、坐标、前端实际渲染的图表的宽高等信息确定验证是否通过方法还需要一个验证成功是否从数据库删除验证码信息的参数总是考虑此人机验证码的安全性确保验证码能拦住机器人不易被破解检查我的提示词若我的要求与安全相悖请提醒我● 全部编译通过。以下是实现总结 --- 点选验证码功能实现总结 新建文件 ┌─────────────────────────────────────┬───────────────────────────────────────────────────────────────────────┐ │ 文件 │ 说明 │ ├─────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤ │ config/captcha.yaml │ 验证码配置元素类型、长度、混淆点数、过期时间 │ ├─────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤ │ internal/infra/captcha/captcha.go │ 核心 APIInit()、Create()、Check()AES-GCM 加解密数据库存储与清理 │ ├─────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤ │ internal/infra/captcha/generator.go │ 图片生成字体加载、文字/图标绘制、元素随机生成 │ └─────────────────────────────────────┴───────────────────────────────────────────────────────────────────────┘ 修改文件 ┌─────────────────────────────────┬───────────────────────────────────────────────────────────┐ │ 文件 │ 改动 │ ├─────────────────────────────────┼───────────────────────────────────────────────────────────┤ │ internal/infra/config/config.go │ 新增 CaptchaConfig 结构体及 Config 中的 Captcha 字段 │ ├─────────────────────────────────┼───────────────────────────────────────────────────────────┤ │ cmd/api/main.go │ 导入 captcha 包并在 database.Init()后调用 captcha.Init()│ ├─────────────────────────────────┼───────────────────────────────────────────────────────────┤ │ go.mod │ 新增依赖 golang.org/x/image │ └─────────────────────────────────┴───────────────────────────────────────────────────────────┘ 架构设计 Create()流程: 随机背景 → 生成N个正确元素 → 生成M个混淆元素 → 打乱全部元素 → 绘制到背景 → Base64编码 → 正确元素序列(AES-GCM加密)→ 存入 Captcha 模型 → 返回{key, width, height, elements[], image_base64}Check()流程: 按key查询 → 检查过期 → 解密正确元素序列 → 验证元素顺序 → 坐标缩放(适配前端渲染尺寸)→ 欧几里得距离容差校验 → 按需删除记录 ⚠️ 安全提醒1. AES 密钥未持久化当前 AES 密钥在 Init()中用 crypto/rand 随机生成服务器重启后无法解密旧验证码。生产环境建议从配置或环境变量读取固定密钥如 captcha.encrypt_key。2. 元素重叠未检测当前不检查元素间是否重叠极端情况下两个元素可能绘制在同一区域导致用户难以点击。后续可增加碰撞检测。3. 坐标加密存储AES-GCM、crypto/rand 安全随机、过期自动清理、容差校验等安全措施已就位可以有效抵御自动化攻击。 ✻ Brewedfor5m 31sAI 新建了config\captcha.yaml配置文件内容如下captcha:# 验证元素类型可选值chinese中文文字、uppercase英文大写字母、icon图标elements:-chinese-uppercase-icon# 验证元素个数用户需要按顺序点击的正确元素数量length:2# 混淆元素个数confusion_length:2# 验证码默认过期时间秒expire:600配置文件得益于精确的提示词是基本满足预期的然后 AI 于main.go中调用了captcha.Init()进行验证码初始化这非常不合理因为验证码和数据库连接等不同它完全可以按需初始化而且所谓的初始化不就是加载配置、清理过期验证码这些操作都是不用着急的最终人工整理出以下第二轮需要修改的点验证码模块无需于main.go提前初始化只调用时初始化即可。生成随机AES密钥并存储于captcha.yaml配置文件中无需单独初始化。无需提前缓存资源文件。无需定期清理过期验证码每次使用到验证码模块时调用一次清理即可。默认配置已经填充至captcha.yaml代码中直接读取使用即可无需再次硬编码这些默认值applyDefaults是多余的增加碰撞检测防止元素重叠。将背景图和ICON图标资源路径配置也放入配置文件并在运行时读取而不是在代码中拼接或硬编码。将新建的internal\infra\captcha\captcha.go和internal\infra\captcha\generator.go尽量精简的整合到一起并改名为internal\infra\captcha\click.go点选验证码数据库操作全部改为GORM的Generics APIgorm.G[Model](db)....而不是现在的Traditional API● 所有改动完成。以下是调整总结......这次打眼一看就舒服多了因为一共就新增了两个文件改动仅配置加载一个文件然后增加了golang.org/x/image依赖主要对internal\infra\captcha\click.go进行review此文件代码行数达到了 600 行先尝试精简单实现点选验证码应该要不了这么多代码实在不行还是只有拆分。review时发现一个问题AI 之所以使用上了AES-256加解密手段是为了将 code 字段加密入库验证时再解密对比关键是 info 字段并未加密该字段包括坐标、元素顺序啥都有所以等于加了个寂寞我提出了质疑以下是 AI 的回复你说得对。我重新评估一下1. 坐标已经明文存在 Info 字段加密 Code 没有额外安全收益有 DB 权限的攻击者直接读 Info 就行2. 验证码的安全性主要靠随机位置、混淆元素、过期时间不是靠加密存储3. 加密/解密增加了不必要的代码复杂度和密钥管理负担这么不靠谱算了就 10 分钟有效期的验证码而且黑客真拖库了哪里还需要刷接口直接取消 AES-256 加密随便用个 md5(md5(code)) 简单处理一下就可以入库了。先让 AI 再整理一波代码清理多余代码保留可阅读力的前提下尽力精简代码比如删去从未使用过的max函数取消 code 字段的 AES 加密改用最简单的 md5(md5(code)) 或 sha1 处理一下就可以入库了ensureInit 函数是多余的其中 aesKey 解码已去除cfg config.Get().Captcha则是需要使用配置数据时再直接使用cfg : config.Get().Captcha即可定期清理过期验证码已经单独调用封装随机数与文件系统管理包完成后由于本次验证码生成已经涉及到随机数生成、文件系统相关了这两样未来都会比较常用所以打算先做封装单独建立对应的包pkg/random/random.gopkg/filesystem/filesystem.go建立以上两个包让后帮我将 internal/infra/captcha/click.go 中的一些函数按分类迁移进去比如cryptoRandInt 迁移至 random并重写为接受 min、max 两个参数的整数随机数生成方法randomTextColor 迁移至 random此方法无需再以 random 为前缀注意不通用的函数无需迁移依赖基础设施的无需迁移如config、database需要迁移的函数总是让它更加通用易扩展而不是让它仅服务于单独的模块比如当前的 captcha新包中暂时用不上的方法无需额外建立// pkg/random/random.go 中经过人工整理之后我们得到了以下非常简洁的随机数生成包packagerandomimport(crypto/randimage/colormath/bigmathrandmath/rand/v2)// Int 返回 [min, max) 范围的加密安全随机整数// 可用 Int(100, 999) 生成 3 位随机数以此类推funcInt(min,maxint)int{ifmaxmin{returnmin}n,err:rand.Int(rand.Reader,big.NewInt(int64(max-min)))iferr!nil{returnmin}returnminint(n.Int64())}// FastInt 返回 [min, max) 范围的快速随机整数// 可用 FastInt(100, 999) 生成 3 位随机数以此类推funcFastInt(min,maxint)int{ifmaxmin{returnmin}returnminmathrand.IntN(max-min)}// RGB 返回随机 RGB 颜色// tone:dark深色,light浅色funcRGB(tonestring)color.Color{iftonelight{returncolor.RGBA{R:uint8(180Int(0,75)),G:uint8(180Int(0,75)),B:uint8(180Int(0,75)),A:255,}}returncolor.RGBA{R:uint8(20Int(0,80)),G:uint8(20Int(0,80)),B:uint8(20Int(0,80)),A:255,}}// pkg\filesystem\filesystem.go 文件packagefilesystemimportpath/filepath// TrimExt 返回去除了路径和扩展名的文件名// path:文件路径或完整文件名funcTrimExt(pathstring)string{name:filepath.Base(path)ext:filepath.Ext(name)returnname[:len(name)-len(ext)]}

相关新闻