微信小程序快捷登录实现原理与代码详解
在移动应用开发中,良好的登录体验对用户留存至关重要。微信小程序提供了快捷登录能力,让用户无需手动输入账号密码,一键完成登录。本文将详细介绍微信小程序登录注册的实现原理和完整代码。
1. 微信小程序登录原理
微信小程序的登录流程主要包括以下几个关键步骤:
前端调用 wx.login() 获取临时登录凭证 code
后端调用微信服务端接口 code2Session,换取用户的 openid、session_key 等信息
前端调用 getPhoneNumber 获取用户手机号授权
后端通过 code 获取用户手机号
后端根据手机号或 openid 完成用户注册或登录,返回业务 token
这种方式既保障了安全性,又大大简化了用户注册登录的流程。
2. 登录流程图解
┌─────────┐ ┌─────────┐ ┌─────────────┐
│ │ │ │ │ │
│ 小程序前端 │ │ 应用服务器 │ │ 微信服务器 │
│ │ │ │ │ │
└────┬────┘ └────┬────┘ └──────┬──────┘
│ │ │
│ wx.login() │ │
├───────────────────▶│ │
│ │ │
│ │ code2Session │
│ ├─────────────────────▶│
│ │ │
│ │ openid,session_key │
│ │◀─────────────────────┤
│ 登录检查结果 │ │
│◀───────────────────┤ │
│ │ │
│ │ │
│ getPhoneNumber() │ │
├───────────────────▶│ │
│ │ getPhoneNumber │
│ ├─────────────────────▶│
│ │ │
│ │ phone_info │
│ │◀─────────────────────┤
│ │ │
│ 登录成功(token) │ │
│◀───────────────────┤ │
│ │ │3. 数据结构设计
3.1 微信用户表
要实现微信登录,首先需要设计一个存储微信用户信息的表,与普通用户表关联:
// WechatUser 微信用户信息表
type WechatUser struct {
gorm.Model
UserID uint json:"user_id" gorm:"index;not null" // 关联到User表的ID
OpenID string json:"openid" gorm:"uniqueIndex;size:40;not null" // 微信openid
UnionID string json:"unionid" gorm:"size:40;default:''" // 微信unionid
SessionKey string json:"session_key" gorm:"size:100" // 微信会话密钥
LastLoginAt time.Time json:"last_login_at" // 最后登录时间
User User json:"-" gorm:"foreignKey:UserID" // 关联用户
}3.2 请求和响应模型
登录检查请求
// WechatLoginCheckReq 微信登录检查请求
type WechatLoginCheckReq struct {
Code string json:"code" binding:"required" // 微信登录返回的临时凭证
}注册请求
// WechatRegisterReq 微信注册请求
type WechatRegisterReq struct {
OpenID string json:"openid" binding:"required" // OpenID
PhoneCode string json:"phone_code" binding:"required" // 获取手机号的凭证
Nickname string json:"nickname" // 用户昵称
AvatarUrl string json:"avatar_url" // 用户头像URL
}4. 核心代码实现
4.1 微信API调用封装
首先,我们需要封装微信API的调用:
// Code2Session 登录凭证校验
func Code2Session(code string) (*Code2SessionResp, error) {
url := fmt.Sprintf(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
globals.AppConfig.Wechat.AppID,
globals.AppConfig.Wechat.AppSecret,
code,
)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("请求微信API失败: %w", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取微信API响应失败: %w", err)
}
var result Code2SessionResp
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析微信API响应失败: %w", err)
}
if result.ErrCode != 0 {
return nil, fmt.Errorf("微信API返回错误: %d %s", result.ErrCode, result.ErrMsg)
}
return &result, nil
}4.2 登录检查逻辑
登录检查是第一步,判断用户是否已注册:
// CheckLogin 检查微信登录状态
func (l WechatLoginLogic) CheckLogin(c context.Context, req requests.WechatLoginCheckReq) (*responses.WechatLoginCheckResp, error) {
// 调用微信API获取openid和session_key
wxResp, err := wechatutil.Code2Session(req.Code)
if err != nil {
return nil, fmt.Errorf("CheckLogin() 调用微信API失败: %w", err)
}
// 根据openid查找用户
user, wechatUser, err := l.wechatUserRepo.GetUserWithWechat(c, wxResp.OpenID)
if err != nil {
return nil, fmt.Errorf("CheckLogin() 查询用户失败: %w", err)
}
// 构建响应
resp := &responses.WechatLoginCheckResp{
OpenID: wxResp.OpenID,
IsRegistered: false,
}
// 如果找到了微信用户和关联的用户
if user != nil && wechatUser != nil {
// 更新session_key
if err := l.wechatUserRepo.UpdateSessionKey(c, wechatUser.ID, wxResp.SessionKey); err != nil {
globals.Log.Errorf("CheckLogin() 更新SessionKey失败: %v", err)
// 不中断流程
}
// 更新用户最后登录时间
if err := l.userRepo.UpdateLastLoginTime(c, user.ID); err != nil {
globals.Log.Errorf("CheckLogin() 更新用户登录时间失败: %v", err)
// 不中断流程
}
// 生成令牌
accessToken, refreshToken, err := l.generateTokens(c, user.ID)
if err != nil {
return nil, fmt.Errorf("CheckLogin() 生成令牌失败: %w", err)
}
resp.IsRegistered = true
resp.UserID = user.ID
resp.AccessToken = accessToken
resp.RefreshToken = refreshToken
}
return resp, nil
}4.3 注册逻辑
如果用户未注册,需要通过手机号进行注册:
// Register 微信注册
func (l WechatLoginLogic) Register(c context.Context, req requests.WechatRegisterReq) (*responses.WechatRegisterResp, error) {
// 获取手机号
phone, err := wechatutil.GetPhoneNumber(c, req.PhoneCode)
if err != nil {
return nil, fmt.Errorf("Register() 获取手机号失败: %w", err)
}
// 开始事务
tx := globals.DB.Begin()
if tx.Error != nil {
return nil, fmt.Errorf("Register() 开始事务失败: %w", tx.Error)
}
var txErr error
defer func() {
if r := recover(); r != nil || txErr != nil {
tx.Rollback()
}
}()
// 处理头像并获取用户信息
var avatarID uint
if req.AvatarUrl != "" {
avatarID, err = l.saveWechatAvatar(c, req.AvatarUrl, tx)
if err != nil {
globals.Log.Errorf("Register() 保存头像失败: %v", err)
// 不中断流程
}
}
// 根据手机号查找用户
user, err := l.userRepo.FindByPhone(c, phone)
if err != nil {
txErr = err
return nil, fmt.Errorf("Register() 查询用户失败: %w", err)
}
if user == nil {
// 创建新用户
user = &models.User{
AccountID: usersignutil.GenerateAccountID(),
Nickname: req.Nickname,
Phone: phone,
SchoolID: req.SchoolID,
Status: 1,
Type: "user",
AvatarID: avatarID,
LastTime: sql.NullTime{Time: time.Now(), Valid: true},
}
if err := tx.Create(user).Error; err != nil {
txErr = err
return nil, fmt.Errorf("Register() 创建用户失败: %w", err)
}
}
// 创建或更新微信用户关联
wechatUser, err := l.wechatUserRepo.FindByOpenID(c, req.OpenID)
if wechatUser == nil {
wechatUser = &models.WechatUser{
UserID: user.ID,
OpenID: req.OpenID,
LastLoginAt: time.Now(),
}
if err := tx.Create(wechatUser).Error; err != nil {
txErr = err
return nil, fmt.Errorf("Register() 创建微信用户关联失败: %w", err)
}
} else {
wechatUser.UserID = user.ID
if err := tx.Save(wechatUser).Error; err != nil {
txErr = err
return nil, fmt.Errorf("Register() 更新微信用户关联失败: %w", err)
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
txErr = err
return nil, fmt.Errorf("Register() 提交事务失败: %w", err)
}
// 生成访问令牌
accessToken, refreshToken, err := l.generateTokens(c, user.ID)
if err != nil {
return nil, fmt.Errorf("Register() 生成令牌失败: %w", err)
}
// 构建响应
userInfo := new(responses.WechatUserInfoResp).ToResponse(user)
resp := &responses.WechatRegisterResp{
UserInfo: userInfo,
AccessToken: accessToken,
RefreshToken: refreshToken,
}
return resp, nil
}4.4 头像处理
微信头像需要特殊处理,以符合系统要求:
func (l WechatLoginLogic) saveWechatAvatar(ctx context.Context, avatarUrl string, tx gorm.DB) (uint, error) {
// 下载微信头像
resp, err := http.Get(avatarUrl)
if err != nil {
return 0, fmt.Errorf("下载微信头像失败: %w", err)
}
defer resp.Body.Close()
// 读取图片数据
imageData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("读取头像数据失败: %w", err)
}
// 生成唯一文件名
filename := fmt.Sprintf("wx_avatar_%s.jpg", uuid.New().String())
// 保存到本地并创建数据库记录
staticPath := globals.AppConfig.StaticConfig.Path
fullPath := staticPath + "/" + filename
err = ioutil.WriteFile(fullPath, imageData, 0644)
if err != nil {
return 0, fmt.Errorf("保存头像文件失败: %w", err)
}
// 构建符合系统格式的URL
domain := globals.AppConfig.App.Domain
port := globals.AppConfig.App.Port
imageUrl := fmt.Sprintf("http://%s:%d/static/%s", domain, port, filename)
// 创建图片记录
image := models.Image{
Url: imageUrl,
Type: "avatar",
}
if err := tx.Create(&image).Error; err != nil {
return 0, fmt.Errorf("创建图片记录失败: %w", err)
}
return image.ID, nil
}4.5 令牌生成
登录成功后需要生成JWT令牌:
func (l *WechatLoginLogic) generateTokens(
c context.Context,
userID uint,
) (accessToken, refreshToken string, err error) {
// 生成访问令牌
accessToken, err = usersignutil.GenerateAccessToken(userID)
if err != nil {
return "", "", fmt.Errorf("generateTokens() 生成访问令牌失败: %w", err)
}
// 生成刷新令牌
refreshToken, err = usersignutil.GenerateRefreshToken(userID)
if err != nil {
return "", "", fmt.Errorf("generateTokens() 生成刷新令牌失败: %w", err)
}
// 将刷新令牌保存到Redis
if err := l.tokenRepo.SaveUserRefreshToken(c, userID,
refreshToken,
globals.AppConfig.JWT.RefreshTokenExpiry); err != nil {
globals.Log.Errorf("generateTokens() 保存刷新令牌到Redis失败: %v", err)
// 不中断流程
}
return accessToken, refreshToken, nil
}5. 最佳实践与注意事项
安全性考虑
敏感数据加密:session_key ,openid等敏感数据应该加密存储
令牌过期策略:设置合理的令牌过期时间,并实现令牌刷新机制
事务处理:使用数据库事务确保数据一致性
性能优化
Redis缓存:使用Redis存储令牌和会话信息
头像处理:异步处理用户头像,不阻塞主流程
连接池管理:合理配置数据库和Redis连接池
用户体验
登录状态保持:使用refreshToken机制延长登录状态
登录失败恢复:在登录失败时提供友好提示和重试机制
6. 总结
微信小程序登录是一种高效、安全的用户认证方式。通过本文介绍的技术方案,可以实现无缝的用户登录体验,同时保障系统安全。关键在于正确理解微信登录机制,并设计合理的系统架构来对接