前置准备
用GoFrame框架进行登录接口的开发,提供以下几种登录方式:
- 手机号+验证码
- 邮箱+验证码
- 微信公众平台扫码登录
在本次开发过程中用到的Goframe框架的工程目录如下:
/
├── api
├── internal
│ ├── cmd
│ ├── controller
│ ├── dao
│ ├── logic
│ ├── model
│ | ├── do
│ │ └── entity
│ └── service
├── manifest
├── resource
├── utility
├── go.mod
└── main.go
开发前需提前安装好gofrmae相应的环境,一般使用gf run main.go
命令来运行程序
开发规范
路由注册
路由注册主要是在internal/cmd/cmd.go
文件中进行,GoFrame框架初始化时就提供了cmd文件的相应模板。
这里我使用了goframe框架v2版本的路由规范以及路由绑定对象的方式进行路由注册。
每个路由对绑定对应的对象的方法NEWxxx(),该对象在控制器controller中进行声明、定义和实现。
下面拿出手机号登录的路由对象以作示例,下面部分代码在internal/controller/login/login.go
文件中
对象声明与实现在上图模板中,下面的两个方法对应的就是该对象的两个路由。
在终端可以看到对应方法实现的路由/login/phone/sendsmscode
和/login/phone/tel-sms-login
。如果我们方法名命名为驼峰命名法,即TelPhoneNum
,除首字符外有大写字母,则会自动将路由中的大写转换为小写并加上-
,如tel-phone-num
服务定义与封装
goframe框架为了降低模块之间的耦合性,分了service
和logic
两个目录来进行业务的相关开发。一般controller
层调用的是service
层封装好的方法,而service
层下接logic
层的服务实现逻辑,所有关于业务处理相关的代码都放在logic
层。
service层模板如下图:
可以看到业务处理逻辑都在ILogin接口中,其它代码为相应的初始化部分,为goframe框架的规范。相应代码在internal/service/login.go
文件中。
logic层模板如下图:
logic层则是对应service层进行一些初始化,首先需要定义一个结构体,并有相应的初始化函数,紧接着便是对应service层中的方法的实现。
手机号+验证码登录
手机号+验证码的登录方式,我们只需要明确一下如何保存验证码和验证验证码即可,我们这里使用Redis数据库,用手机号+验证码的键值对进行保存。同时我们需要给对应手机号发送验证码。手机号需要进行相应的鉴别,确保手机号是有效的。
UniSMS调用发送短信
实现手机号登录需要调用第三方的短信验证平台,这里选用的是UniSMS。具体调用步骤查看对应文档
在接入SDK之后需要进行签名认证,如果是个人使用要用个人姓名进行申请,否则通过不了。
相应发送信息代码如下:
//以下为调用unisms平台的SDK
client := unisms.NewClient("RQ21CBsYy41DkG8hfrHEZtFRiT4mLWkPo2npHZFQpv8agmL7o", "your access key secret") // 若使用简易验签模式仅传入第一个参数即可
// 构建信息
message := unisms.BuildMessage()
message.SetTo("%s", Phonenumber)
message.SetSignature("吴奇墉") //签名
message.SetTemplateId("pub_verif_ttl3") //短信模板
message.SetTemplateData(map[string]string{"code": verificationCode}) //验证码
_ = client
//发送短信
res, err := client.Send(message)
_ = res
if err != nil {
g.Log().Error(ctx, "发送验证码失败:", err)
return err
}
验证码生成与验证
在发送信息接口生成验证码并进行存放进缓存中,设置过期时间为15分钟
// 生成随机数
verificationCode := fmt.Sprintf("%06d", rand.Intn(1000000))
//验证码保存在Redis缓存中
expiration := 15 * time.Minute
_, err := g.Redis().Do(ctx, "SETEX", Phonenumber, int64(expiration.Seconds()), verificationCode)
if err != nil {
g.Log().Error(ctx, "验证码Redis操作错误:", err)
return err
}
在验证登录接口取出验证码并进行验证
//从redis获取验证码
v, err := g.Redis().Do(ctx, "GET", Phonenumber)
//打印日志
g.Log().Info(ctx, "Redis中的验证码为:", v)
if err != nil {
g.Log().Error(ctx, "读取Redis中验证码失败:", err)
return "", err
}
//验证验证码是否匹配
if v.String() != smscode {
err = errors.New("smscode don't match")
g.Log().Error(ctx, "验证验证码失败:", err)
return "", err
}
用户信息放入mysql数据库
err = dao.User.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err = dao.User.Ctx(ctx).Data(do.User{
Phonenumber: Phonenumber,
}).Insert()
return err
})
if err != nil {
return "", err
}
邮箱号+验证码登录
邮箱号+验证码的登录方式跟手机号+验证码类似,只是将发送短信步骤改为发送邮件。对于邮箱号需要进行格式验证,确保邮箱有效。
获取授权码
这里以QQ邮箱为例
将生成的授权码进行复制
gomail库发送邮件并验证
"gopkg.in/gomail.v2"
为本次发送邮件使用的库。
官方文档:https://pkg.go.dev/gopkg.in/gomail.v2
发送邮件的内容需要我们自行编辑
在发送邮件接口:
//发送邮件
m := gomail.NewMessage()
m.SetHeader("From", "发送人")
m.SetHeader("To", "接收人")
m.SetHeader("Subject", "标题")
m.SetBody("text/html", "正文")
d := gomail.NewDialer("smtp.qq.com", 465, "xxx@qq.com", "授权码")
err = d.DialAndSend(m)
if err != nil {
g.Log().Info(ctx, "发送邮件失败:", err)
return false, err
}
验证码存入Redis
//验证码保存在Redis缓存中
expiration := 15 * time.Minute
_, err := g.Redis().Do(ctx, "SETEX", emailinput.Emailnumber, int64(expiration.Seconds()), smscode)
if err != nil {
g.Log().Error(ctx, "验证码Redis操作错误:", err)
return false, err
}
发送后就能收到邮件
验证登录接口,从Redis取出验证码进行比对
//从redis获取验证码
v, err := g.Redis().Do(ctx, "GET", emaillogininput.Emailnumber)
//打印日志
g.Log().Info(ctx, "Redis中的验证码为:", v)
if err != nil {
g.Log().Error(ctx, "读取Redis中验证码失败:", err)
return "", err
}
微信公众平台测试号获取登录二维码+前端轮询+扫码登录
微信扫码登录有两种方式,一种是通过微信公众平台进行开发,另一种是通过微信开放平台进行开发。这里用微信公众平台进行开发,微信公众平台能够申请到测试号。
微信公众平台开发步骤
1、测试号申请
2、配置开发者身份
3、使用appid
和appsecret
获取访问凭证access_token
4、使用access_token
获取二维码ticket
5、使用ticket
获取二维码链接
6、编写html文件显示二维码并进行轮询
7、处理用户扫码消息推送
8、发送ticket
到检查登录状态接口,查询该ticket
下是否有对应的openid
测试号申请
申请链接:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
扫码通过微信账号登录即可
配置开发者身份
配置开发者身份可以参考微信公众平台的开发文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
需要准备一个域名进行身份验证,如果没有域名的可以购买一个云服务器,也可以看看阿里云服务器的免费试用。
在接口配置信息中填入接收参数,Token可以自定义,URL中填写域名+端口号+接口地址(图中localhost只是演示,需替换为真实域名或服务器公网ip)
文档中提示:
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
参数 详情 signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 timestamp 时间戳 nonce 随机数 echostr 随机字符串 开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,
请原样返回echostr参数内容
,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
另请注意,微信公众号接口必须以http://或https://开头
,分别支持80端口和443端口。
基于文档,编写代码对微信服务器发来的消息进行签名验证
r := g.RequestFromCtx(ctx)
signature := r.URL.Query().Get("signature")
timestamp := r.URL.Query().Get("timestamp")
nonce := r.URL.Query().Get("nonce")
echostr := r.URL.Query().Get("echostr")
sign := &model.Signature{
Signature: signature,
Timestamp: timestamp,
Nonce: nonce,
Echostr: echostr,
}
g.Log().Debug(ctx, "签名内的内容:", sign)
//签名验证逻辑
bool1, err := checksign.CheckSignature(ctx, sign)
if err != nil {
g.Log().Error(ctx, err)
}
CheckSignature
函数:
r := g.RequestFromCtx(ctx)
token := model.TOKEN
tmpArr := []string{token, sign.Timestamp, sign.Nonce}
sort.Strings(tmpArr)
tmpStr := ""
for _, str := range tmpArr {
tmpStr += str
}
hash := sha1.New()
hash.Write([]byte(tmpStr))
tmpSum := hash.Sum(nil)
tmpStr = fmt.Sprintf("%x", tmpSum)
r.Response.WriteString(sign.Echostr)
return tmpStr == sign.Signature, nil
获取access_token
我们使用测试号中申请的appid
和appsecret
获取access_token
,获取access_token
的相关步骤参考官方链接https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
获取access_token的方式是通过GET的方式从微信服务器进行获取,调用的链接为:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
GET请求携带参数如下:
参数 | 是否必须 | 说明 |
---|---|---|
grant_type | 是 | 获取access_token填写client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,即appsecret |
返回参数:
参数 | 说明 |
---|---|
access_token | 获取到的凭证 |
expires_in | 凭证有效时间,单位:秒 |
代码如下:
//获取access_token
resp, err := g.Client().Get(ctx, model.Accesstoken_url)
if err != nil {
g.Log().Error(ctx, err)
}
defer resp.Close()
body1 := []byte(resp.ReadAllString())
if err := json.Unmarshal(body1, &model.WechatToken); err != nil {
fmt.Println("解析 JSON 时出错:", err)
}
g.Log().Debug(ctx, "Access_Token:", model.WechatToken.AccessToken)
return model.WechatToken.AccessToken
获取二维码ticket
我们通过access_token
可以向微信服务器获取ticket
用于生成二维码,在这里我们生成临时二维码。相应操作参考官方文档:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
获取ticket的方式是通过POST的方式从微信服务器进行获取,调用的链接为:
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
POST的数据格式为json,例子:{“expire_seconds”: 604800, “action_name”: “QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}}
POST参数说明:
参数 | 说明 |
---|---|
expire_seconds | 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为60秒。 |
action_name | 二维码类型,QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值 |
action_info | 二维码详细信息 |
scene_id | 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1–100000) |
scene_str | 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64 |
返回参数:
参数 | 说明 |
---|---|
ticket | 获取的二维码ticket,凭借此ticket可以在有效时间内换取二维码。 |
expire_second | 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。 |
url | 二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片 |
代码如下:
//利用access_token获取二维码
jsondata, err := json.Marshal(model.Qr)
if err != nil {
g.Log().Error(ctx, err)
return QrRes, err
}
Qrcode_url := fmt.Sprintf(model.Qrcode_url + s.GetAccesstoken(ctx))
g.Log().Debug(ctx, "获取ticket的url为:", Qrcode_url)
body, err := g.Client().Post(ctx, Qrcode_url, jsondata)
if err != nil {
g.Log().Error(ctx, err)
return QrRes, err
}
var Qrbody model.QrResstruct
body1 := []byte(body.ReadAllString())
if err := json.Unmarshal(body1, &Qrbody); err != nil {
fmt.Println("解析 JSON 时出错:", err)
return QrRes, err
}
使用ticket获取二维码链接
我们获取了ticket
之后可以通过ticket向微信服务器发送GET请求,获取二维码链接。参考官方文档:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
获取二维码链接的方式是通过GET的方式从微信服务器进行获取,TICKET记得进行UrlEncode
,调用的链接为:
https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET
这里我们将ticket存入Redis数据库中,并设置键值对为TICKET:“none”,为了后续前端进行轮询。
代码如下:
//g.Log().Debug(ctx, "获取到的ticket:"+Qrbody.Ticket)
_, err = g.Redis().Do(ctx, "SET", Qrbody.Ticket, "none")
if err != nil {
g.Log().Error(ctx, err)
return QrRes, err
}
//返回生成的二维码链接
url_code := model.GetUrl(Qrbody.Ticket)
QrRes = model.QrRes{
Urlticket: url_code,
Ticket: Qrbody.Ticket,
}
编写html文件显示二维码并进行轮询
在goframe工程目录下
├── resource
│ ├── public
│ │ │──html
创建一个login.html文件,需要在cmd.go
文件中进行静态资源的绑定
需要两个后端接口:获取二维码接口/getqrcode和检查登录状态接口/checklogin
/getqrcode返回响应:
{
"code": 0,
"message": "",
"data": {
"Urlticket": "xxx",
"Ticket": xxx"
}
}
/checkinlogin返回响应:
{
"code": 54,
"message":"",
"data": {
"Message": "xxx"
}
}
前端页面需求:
- 将Urlticket的链接参数进行显示
- 将Ticket参数传递给检查登录状态函数,并发送给后端
- 进行轮询检查登录状态
- 根据后端/checklogin接口响应确定页面显示,wait for login为等待登录,login success为登陆成功
html文件代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信登录</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background-color: #f2f2f2;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.message {
margin-bottom: 20px;
font-size: 18px;
color: #333;
}
#qrcode {
display: none;
max-width: 300px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
#login-success {
display: none;
font-size: 24px;
color: #4caf50;
}
</style>
</head>
<body>
<div class="container">
<div id="message" class="message">等待用户扫码...</div>
<img id="qrcode" src="" alt="微信二维码">
<div id="login-success">登录成功</div>
</div>
<script>
async function fetchQRCode() {
try {
const response = await fetch('getqrcode的后端接口');
const data = await response.json();
if (data.code === 0) {
document.getElementById('qrcode').src = data.data.Urlticket;
document.getElementById('qrcode').style.display = 'block';
pollLoginStatus(data.data.Ticket);
} else {
document.getElementById('message').innerText = '获取二维码失败,请稍后重试';
}
} catch (error) {
document.getElementById('message').innerText = '请求失败,请稍后重试';
}
}
async function checkLogin(ticket) {
try {
const response = await fetch('检查登录状态的接口', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ticket })
});
return await response.json();
} catch (error) {
return { data: { Message: 'wait for login' } };
}
}
async function pollLoginStatus(ticket) {
const checkInterval = setInterval(async () => {
const response = await checkLogin(ticket);
if (response.data.Message === 'login success') {
clearInterval(checkInterval);
document.getElementById('message').style.display = 'none';
document.getElementById('qrcode').style.display = 'none';
document.getElementById('login-success').style.display = 'block';
}
}, 3000); // 轮询时间设置为3秒
}
fetchQRCode();
</script>
</body>
</html>
处理用户扫码消息推送
用户扫描前端页面上的二维码后,微信服务器会进行消息推送,消息将会推送到我们进行签名验证的接口上。文档中说明:
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。
参考官方文档中扫描带参数二维码事件
:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
消息推送会发送XML数据包,数据包内参数如下:
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方账号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,event |
Event | 事件类型,SCAN |
EventKey | 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id |
Ticket | 二维码的ticket,可用来换取二维码图片 |
我们在签名验证接口下添加处理消息推送部分的代码,将接收到的ticket
和openid
以键值对的形式存到Redis数据库中,在后续的检查登录状态接口进行验证。处理消息推送时echostr为空字符串,所以可以进行判断,否则下次进行签名验证将会同时进行处理消息推送事件:
//用户扫码时echostr为空
if sign.Echostr == "" {
g.Log().Info(ctx, "######处理消息推送事件######")
//处理消息推送事件
msg := new(model.XMLMessage)
if err := r.Parse(msg); err != nil {
g.Log().Error(ctx, err)
return false
}
userinfo := model.GetuserInfo{}
//g.Log().Debug(ctx, "xml文件:", msg)
if msg.Ticket != " " && (msg.Event == "SCAN" || msg.Event == "subscribe") {
userinfo.OpenID = msg.FromUserName
expiration := 3 * time.Minute
if msg.FromUserName != " " {
_, err := g.Redis().Do(ctx, "SETEX", msg.Ticket, int64(expiration.Seconds()), msg.FromUserName)
if err != nil {
g.Log().Error(ctx, err)
return false
}
}
return true
}
}
检查登录状态接口
我们在处理消息推送事件时将ticket
和openid
存入Redis数据库,现在根据前端发送的ticket
进行验证,如果取出的有openid
,则表示用户已经扫码,登陆成功;如果取出的是"none",则表示还没有用户进行扫码。
g.Log().Info(ctx, "######检查登录状态######")
r := ghttp.RequestFromCtx(ctx)
Check := model.Check{}
jsonData := r.GetBody()
if err := json.Unmarshal([]byte(jsonData), &Check); err != nil {
fmt.Println("解析 JSON 数据失败:", err)
g.Log().Error(ctx, err)
return err
}
openid, err := g.Redis().Do(ctx, "GET", Check.Ticket)
if err != nil {
g.Log().Error(ctx, "获取Redis中的openid失败:", err)
return err
}
g.Log().Debug(ctx, "openid=", openid.String())
if openid.String() == "none" {
err = gerror.New("have no user login")
g.Log().Info(ctx, "wait for login...")
return err
}
获取用户信息(if needed)
jwt鉴权,返回访问token
JWT本质上是一组字符串,又Header、Payload和Secret三部分组成。
Header: 定义了生成签名的算法以及Token的类型
Payload: 用来存放实际传递的数据
Signature: 服务器通过Header、Payload和Secret,使用Header中生成签名的算法生成
Payload部分默认是不加密的,所以不要将隐私信息放在Payload当中
JWT身份验证流程
图片来源:JavaGuide
- 用户向服务器发送用户名、密码以及验证码用于登陆系统。
- 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
- 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
- 服务端检查 JWT 并从中获取用户相关信息。
相关代码
这里使用的是自己写的一个生成JWT令牌的代码,也可以使用goframe中的gf-jwt
库或者go-jwt
库
gf-jwt官方文件:https://goframe.org/pages/viewpage.action?pageId=6357048
go-jwt官网:https://pkg.go.dev/github.com/golang-jwt/jwt/v5
func GenerateJWT(ctx context.Context, Phonenumber string, Expirationtime time.Time) (string, error) {
// 创建一个新的Token对象
token := jwt.New(jwt.SigningMethodHS256)
// 设置Payload部分,这里以手机号作为Payload
claims := token.Claims.(jwt.MapClaims)
claims["Phonenumber"] = Phonenumber
claims["exp"] = Expirationtime.Unix()
// 使用密钥进行签名生成Token字符串
tokenString, err := token.SignedString(model.SecretKey)
if err != nil {
return "", err
}
return tokenString, nil
}
参考文章:
【微信公众平台】扫码登陆:https://blog.csdn.net/qq_46449962/article/details/137951715?spm=1001.2014.3001.5501
基于公众号的微信扫码登陆实现:https://nodejh.com/posts/wechat-scan-qr-code-to-login/
JavaGuide:https://javaguide.cn/system-design/security/basis-of-authority-certification.html
微信扫码登录:https://developers.weixin.qq.com/community/develop/article/doc/0000a8a4f8c91040f70c9a8425c013
微信扫码登录详细操作流程(微信公众平台开发):https://blog.csdn.net/weixin_43890049/article/details/119463862