网站Logo 90的blog

微信小程序注册登录实现

root
18
2024-07-04

微信小程序快捷登录实现原理与代码详解

在移动应用开发中,良好的登录体验对用户留存至关重要。微信小程序提供了快捷登录能力,让用户无需手动输入账号密码,一键完成登录。本文将详细介绍微信小程序登录注册的实现原理和完整代码。

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. 总结

微信小程序登录是一种高效、安全的用户认证方式。通过本文介绍的技术方案,可以实现无缝的用户登录体验,同时保障系统安全。关键在于正确理解微信登录机制,并设计合理的系统架构来对接

动物装饰