当我使用小程序码登录网站时,我发现了一个问题,在手机端不方便登录。我们都知道手机号验证码可以登录网站,但是我没有资源去实现手机号验证码功能,我使用一个变通的方案,在手机端不使用手机号验证码也能登录。
小程序实现验证码登录
它的核心还是鉴权,我在小程序端制作了一个获取验证码界面,它可以生成模拟编号和验证码。当用户点击获取验证码时,会向后台请求返回编号和验证码,后台这个时候会记录为哪个用户openid生成了哪个验证码。那为什么不单单使用验证码呢?还要再添加一个编号,不麻烦吗?因为单单使用验证码会发生撞车的可能。要知道,验证码一般4位或者6位,很容易被暴力攻击的。
当用户在网站端输入编号和验证码时,后端会校验是否存在这对编号和验证码,如果校验正确,将取出openid并绑定到sessionID上,然后返回给前端,存入cookie中。可以看看前面的文章,扫描二维码登录,同样的道理。
除了在小程序生成验证码外,我们还可以在公众号中发送消息获取验证码。它其实也是使用了微信的账号体系。
公众号实现验证码登录
要知道公众号不仅是内容分发平台,更是一个强大的身份认证中间件。我们使用发送消息来获取验证码信息。
1. 握手与回调 (Webhook)
当你在公众号消息发送验证码三个字时,微信会推送事件到你的服务器。我使用go对接了微信公众号消息。
要在 Go 中接收微信消息,你必须先在微信公众平台配置一个 服务器地址 (URL)。** 微信会向你的 URL 发送一个 GET 请求,包含签名、随机数等。你必须按照规定的算法计算并返回正确的 echostr,这被称为“服务器验证”。 验证通过后,每当用户发送消息,微信服务器就会以 POST 方式将消息体推送给你。
2. 解析 XML 数据
与现代 API 不同,微信公众号的推送采用的是 XML 格式。Go 的标准库 encoding/xml 提供了强大的解析能力:
type WxMsg struct {
ToUserName string `xml:"ToUserName"`
FromUserName string `xml:"FromUserName"` // 这就是用户的 OpenID
Content string `xml:"Content"` // 用户发来的文字
}
3. 验证码生存逻辑
当用户发送“验证码”关键字时,我们的 Go 后端会生成一个 4-6 位的随机数和一个编号。并将消息中的 OpenID 与 编号和随机数 存入 Redis,并设置 TTL(如 5 分钟)。然后通过 XML 响应将验证码发回给用户。
4. 身份绑定,完成登录
我们在Web HTML页面提供了登录表单,提供编号和验证码输入框。用户输入编号和验证码后提交到后端,后端从 Redis 中根据编号和验证码反查 OpenID,若存在且有效,则代表身份验证成功,返回包含身份的TOKEN。
在考虑手机验证码的时候,我正在研究Asterisk,顺便实现了一个语音验证码。这个语音验证码没有对接电信系统,所以体验上差点意思。但这不妨碍我们研究其原理。
我实现语音验证码的思路如下:
1,制作了一个小程序页面,上面有一个按钮就叫语音验证码。
2,当点击获取语音验证码按钮时,会请求服务端,获取该用户的openid,然后查找其绑定的VOIP账号。
3,当找到账号后,会生成编号和验证码,主键是编号和验证码,值为openid。
4,后端go服务会调用ARI,传递编号和验证码,呼叫openid对应的VOIP账号。
5,如果此时它的客户端在线,就会振铃,接听后会播报语音验证码和编号。
6,用户在Web页面填入听到的验证码和编号,点击登录。
7,后端会根据它查找对应的openid,生成token,返回给浏览器。
8,后续的浏览器请求会携带这个token,我们就知道是谁?这就完成了登录。
在实现的过程中,我们发现发送语音请求通常需要 1-3 秒才能得到Asterisk的响应。在 Go 中,我们绝不能让主处理函数在那里“傻等”。
通过使用 Goroutine,我们可以瞬间返回“已发起呼叫”的信号给小程序前端,而真实的 API 请求在后台悄悄进行。
// 异步发起语音呼叫
go func(phone string, code string) {
err := smsService.Call(phone, code)
if err != nil {
log.Println("语音播报失败:", err)
}
}(userPhone, verifyCode)
我没有对接电信系统,如果对接了电信系统,这个是需要收费的。语音呼叫成本较高且容易被攻击骚扰用户。在逻辑中,我们必须通过 Redis 实施严格的限制:
- 同一手机号 60 秒内只能获取一次。
- 同一 IP 每天限制获取 5 次。
如果你喜欢这项技术,可以点击登录体验一下。