🥈效果图
📗开发背景
为什么想用微信扫码登录呢?
起因是自己开发了一个搜题网站,内容很简单,但是没有登陆,所以游客可以随便使用,当然也不是不让游客访问,只是没有登陆的话,不能很好的统计使用的用户,也能减少些一些滥用的用户。
起初,我是想设计成账号密码登录网站的,但是想了下,我自己平常碰到一些需要注册的网站,我往往会直接跳过,就不会对这个网站感兴趣了,能让我感兴趣的网站一般都是支持直接扫码登录或者可以以第三方账号直接注册登录,所以能吸引更多的用户,必须要把这个门槛给打下来,提高用户体验!
📖寻找方案,以及选择哪种方案
于是,我开始踏上了百度之旅,经过数次的查阅资料,发现有三种方式实现微信扫码登录
-
第三方网站(https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html) -
公众号 -
小程序
第三方网站
第三方网站方式是直接请求api,https://open.weixin.qq.com/connect/qrconnect
带上下面的参数 例子:携带参数的链接
页面会重定向到一个附带临时code的地址
用户扫码成功后,页面会自动跳转到redirect_uri
指定的链接,这样就完成了扫码登录
总之,微信开放平台的方式应该是最舒服的微信扫码登陆了,但是前提需要交认证300元的认证费用,网站的话还需要提交《微信开放平台网站信息登记表》,也是审核最麻烦的方式,只好先pass啦,
公众号
必要条件:公众号是已认证好的服务号流程:
-
利用WxLogin获取到code,再用code通过 https://api.weixin.qq.com/sns/oauth2/access_token
获取access_token; -
拿到access_token之后,可以利用生成二维码api(可携带scene值,就是自己定义的uuid之类的,可以用来区分是哪个网页端发起的扫描)生成微信二维码;
-
当用户扫描生成好的二维码时,微信会根据推送事件到服务端(这个需要自己在后台配置),并且会携带好之前的scene值,后台就可以判断是哪个网页端扫了这个二维码,后台进行登录操作, -
网页端可以通过拿着 scene
值轮训后台的接口,查询是否登录成功 或者利用websocket,后台主动通知你给网页,是否登录成功。
生成二维码文档地址:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
这种方式还是对个人开发者要求太高了,个人就不能申请服务号,认证也要一笔费用,也劝退我了,pass!
小程序
条件:已经上线的小程序(个人/公司)流程:
-
后台调用微信接口 https://api.weixin.qq.com/cgi-bin/token
获取access_token,access_token只有两个小时,因为有调用频率限制,access_token最好是保存在缓存中; -
用户在网页端点击获取小程序码,前端携带随机数请求后台,后台利用access_token获取小程序码 https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=
(其中需要携带一个参数scene
需要注意,scene
值就是一个随机数,需要在前端就生成好,用来后台区分网页端用),返回小程序码二进制数据; -
前端接收后台返回的二进制数据,(此时前端需要不停地轮训后台接口查询是否登录成功,或者利用websocket,后台主动通知前端)展示小程序码,用户拿出手机用微信扫码之后,类似下面截图,
-
用户主动点击登录按钮,登录按钮需要绑定WxLogin事件,获取用户的微信code,再调用后台提供的登录接口进行登录操作,当登陆成功完毕,通知前端完成登录
获取小程序码官方文档 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getQRCode.html
最后,整个过程全程免费,对于我来说也是比较简单的,我先说明下,我之前是开发了一个小程序的,所以对我来说是比较方便的,如果大家没有已经上线的小程序,为了一个扫码登录,确实有点费力
🦯原理
原理图
👞开发步骤
这里我就不细讲了,知道了思路,其实开发就是最简单的事了,这里贴一些关键的代码
网页端
// 获取二维码
getQrcode = () => {
// 创建websocket连接,为了后台能主动通知到前端
if (this.state.ws == null) {
this.setState({
ws: this.createWebSocket(this)
});
}
// 发送请求 获取二维码
this.setState({
qrcodeBase64: ''
});
// 携带uuid到后台,后台将uuid和socket对象关联,方便通知到哪个网页端
getUnlimitedQrCode({ "uuid": this.state.uuid }).then((res) => {
if (res == undefined || res.data == undefined || res.data.data == undefined) {
console.log("获取二维码失败");
return null;
}
this.setState({
qrcodeBase64: res.data.data
});
}).catch((error) => {
console.error(error);
});
}
后台
// 获取不限制的小程序码接口
func (h *handler) GetUnlimitedQRCode() core.HandlerFunc {
return func(c core.Context) {
param := new(RequestQrCodeParam)
c.ShouldBindJSON(¶m)
// 获取accessToekn
accesstokenInfo, err := h.getAccessToken()
if err != nil {
c.AbortWithError(core.Error(
http.StatusBadRequest,
500,
"获取微信access_token失败").WithError(err),
)
return
}
qrCodeBytes := postUnlimitedQRCode(accesstokenInfo, param.Uuid)
// c.ResponseWriter().Write(qrCodeBytes)
var q = QrcodeRes{Data: qrCodeBytes, Errcode: 0, Errmsg: "success"}
c.Payload(q)
}
}
// websocket连接回调函数
var (
err error
server socket.Server
SocketManager = make(map[string]*websocket.Conn) // 存储uuid和socket对象的关系
)
func (h *handler) Connect() core.HandlerFunc {
return func(ctx core.Context) {
server, err = socket.New(h.logger, h.db, h.cache, ctx.ResponseWriter(), ctx.Request(), nil)
if err != nil {
return
}
// 保存uuid到map,后续后台主动回调前台,需要根据uuid找到对应的websocket连接
uuid := ctx.Request().URL.Query().Get("uuid")
log.Println(uuid)
conn, err := server.GetConn()
if err != nil {
return
}
SocketManager[uuid] = conn
go server.OnMessage()
}
}
// 登录接口
func (h *handler) Wx_web_login() core.HandlerFunc {
return func(c core.Context) {
param := new(RequestWebLoginParam)
c.ShouldBindJSON(¶m)
// 登录操作
ticketRes, err := registerAndExportToken(h, c, &WxLoginParams{Code: param.Code})
if err != nil {
c.AbortWithError(core.Error(
400,
500,
"微信登陆失败").WithError(err),
)
}
log.Print(ticketRes)
// 登陆成功后,主动调用websocket的接口,返回token
res := new(webmessage.WebSocketResponse).Success(webmessage.LOGIN_SUCCESS_TYPE, ticketRes)
// 根据uuid找到对应socket
socket := webmessage.SocketManager[param.Uuid]
socket.WriteJSON(res)
c.Payload(res)
}
}
小程序端
// 登录按钮绑定的登录事件
handleLogin = () => {
// 处理登录逻辑
console.log('登录按钮被点击')
// 获取code
Taro.login({
success: (res) => {
console.log(res)
if (res.code != '') {
// 请求登录操作
webWxLogin({ "code": res.code, "uuid": this.state.scene }).then((res) => {
console.log(res.data)
if (res.data.code == 200 && res.data.type == 20000) {
// 登录成功
Taro.showToast({
title: '登录成功',
icon: 'success'
})
// 跳转首页
Taro.switchTab({
url: '/pages/home/index'
})
} else {
Taro.showToast({
title: '登录失败',
icon: 'none'
})
}
}).catch((err) => {
console.log(err)
Taro.showToast({
title: '登录失败',
icon: 'none'
})
})
} else {
Taro.showToast({
title: '登录失败',
icon: 'none'
})
}
},
fail: (err) => {
console.log(err)
Taro.showToast({
title: '登录失败',
icon: 'none'
})
}
})
}
// 这段代码是用GitHub Copilot 写的,真的是把所有情况都写出来了😄
😄总结
这次扫码登录前前后后花了我五天的时间(下班后弄得),其中也碰了不少坑,前端二维码不显示、后台不返回二进制数据、nginx代理的https域名网站websocket也要做特殊处理,真的是在里面爬了很久,虽然这个手段不是很正规,但是过程能学到一些知识,实现了自己想要的效果,还是挺满意的,谢谢看到这里,如果哪里有说的有问题,欢迎指正
via https://mp.weixin.qq.com/s?src=11×tamp=1693313674&ver=4742&signature=BfNfUxpYs7z0vjq1RWjtAvYYHMJwFKjepzAX*SjU8UdbR3WY9SozFRqx9OeXqCMKtJJSPygg7Xj1WmuI0vm4bJ9nAulVwkJM8OUfs3Bg3m4mUDZQ6sHvMMAtJph9hsUL&new=1