微信授权小程序的全过程

1. 登录流程概述:

  • 前端通过 wx.login() 获取临时登录凭证 code
  • 将 code 发送到后端服务器
  • 后端通过 code 换取用户的 openid 和 session_key
  • 后端生成 JWT token 返回给前端
  • 前端存储 token 用于后续请求的身份验证

2. 前端登录实现:

wx.getUserProfile是微信专门用来获取授权人信息的api,参数有2个:

  • 1是的desc,告诉接口你拿登录信息做什么?

  • 2.success是拿到登录信息以后要做什么,就是一个回调函数。

在回调函数里面做了2件事情:

  • 1.向本地服务发送登录请求

  • 2.获取到本地的token以后,缓存在本地

js 复制代码
// 调用微信登录
wx.login({
  success: (res) => {
    if (res.code) {
      // 获取用户信息
      wx.getUserProfile({
        desc: '用于完善会员资料',
        success: (userInfo) => {
          // 发送 code 和用户信息到后端
          wx.request({
            url: 'api/user/login',
            method: 'POST',
            data: {
              code: res.code,
              userInfo: userInfo.userInfo
            },
            success: (loginRes) => {
              // 保存 token
              wx.setStorageSync('token', loginRes.data.token)
            }
          })
        }
      })
    }
  }
})

代码拆开如下所示:

js 复制代码
      const { code } = await new Promise((resolve, reject) => {
        wx.login({
          success: resolve,
          fail: reject
        });
      });

      // 2. 获取用户信息
      const userProfileRes = await new Promise((resolve, reject) => {
        wx.getUserProfile({
          desc: '用于完善会员资料',
          success: resolve,
          fail: reject
        });
      });

微信返回的数据如图所示:

js 复制代码
{
  userInfo: {
    nickName: "用户昵称",
    avatarUrl: "头像URL",
    gender: 1,  // 性别 0:未知、1:男、2:女
    country: "国家",
    province: "省份",
    city: "城市",
    language: "zh_CN"  // 语言
  },
  rawData: "原始数据字符串",
  signature: "签名",
  encryptedData: "加密数据",
  iv: "加密算法的初始向量"
}

wx.login会返回一个临时的code,这个 code 不能直接用来识别用户, 后端需要用这个 code 去微信服务器换取 openid 和 session_key, openid 才是用户的真实唯一标识。

也就是说在前端你用wx.login和wx.getUserProfile获取到的信息,都是用户公开信息,不能用来做用户的唯一标识,真正的唯一标识是openid,还是得有小程序的后端代码获取。

3. 后端处理流程:

从代码中可以看到,后端的登录处理主要包含以下步骤:

在前端wx.login里面你拿到了用户的临时code,然后你通过本地后台的'/user/login接口传给后端,后端拿到你的code以后就会调用微信服务,通过微信服务拿到sessionInfo里面的openid。

在你的数据库User表里面一定有一个字段是openid,用来搜索用户信息的。如果User里面有这个openId就说明它登录了,没有那就要后端把这个用户及它的openid存到数据库里面去。(用户信息你可以通过前端和code一起传过来)

js 复制代码
// 调用微信登录服务
const wechatService = new (require('../services/wechat'))();
const sessionInfo = await wechatService.code2Session(code);

if (!sessionInfo || !sessionInfo.openid) {
  throw new Error('获取微信用户信息失败');
}

console.log('微信登录成功,openId:', sessionInfo.openid);

// 查找或创建用户
let user = await User.findOne({ where: { openId: sessionInfo.openid } });

4. 身份认证机制:

微信用户信息创建更新完以后,我们需要获取token,这个件事也是在后端登录接口里面做的。

4.1.后端获取token

一般我们通过第三方库拿到jwt实例,然后再去认证token(身份令牌),比如在node服务里面,我们用的是jsonwebtoken

js 复制代码
const jwt = require('jsonwebtoken');
  • 使用 JWT(JSON Web Token)进行身份验证:
js 复制代码
// 生成JWT token
const token = jwt.sign(
  {
    userId: user.userId,   // 用户ID
    openId: user.openId    // 微信openId
  },
  // 第二个参数:密钥
  process.env.JWT_SECRET || 'your_jwt_secret_key',
  // 第三个参数:配置选项,7d表示: token有效期7天
  { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);

JWT格式

js 复制代码
// JWT格式:header.payload.signature
// 例如:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJ1c2VySWQiOjEsIm9wZW5JZCI6Im94eHh4IiwiaWF0IjoxNTE2MjM5MDIyfQ.
// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

小程序后端的login请求结束以后会把token返回出去

js 复制代码
      ctx.body = {
        code: 200,
        message: '登录成功',
        data: {
          token,
          userInfo: {
            userId: user.userId,
            nickName: user.nickName,
            avatarUrl: user.avatarUrl,
            phoneNumber: user.phoneNumber,
            openId: user.openId,
            balance: user.balance || 0,
            status: user.status,
            createTime: user.createTime,
            lastLoginTime: user.lastLoginTime
          }
        }
      };

4.2前端拿到token以后需要做的事情

前端拿到token要做三件事,1.缓存token,2.发送请求的时候把token带上,3.后端api验证token

4.2.1缓存token

在前端里面我们用const app = getApp();拿到小程序的全局变量,这个getApp都做了什么事情

js 复制代码
// app.js
App({
  globalData: {
    userInfo: null,      // 用户信息
    hasUserInfo: false,  // 是否已登录
    token: null,         // 登录token
    baseUrl: 'http://localhost:3000/api',  // API基础地址
    selectedCategoryId: null  // 选中的分类ID
  },
  
  // 检查登录状态
  checkLoginStatus() {
    try {
      const token = wx.getStorageSync('token');
      const userInfo = wx.getStorageSync('userInfo');
      
      if (token && userInfo) {
        this.globalData.token = token;
        this.globalData.userInfo = userInfo;
        this.globalData.hasUserInfo = true;
        return true;
      }
      return false;
    } catch (error) {
      return false;
    }
  },
  
  // 更新登录状态
  updateLoginStatus(token, userInfo) {
    if (token && userInfo) {
      this.globalData.token = token;
      this.globalData.userInfo = userInfo;
      this.globalData.hasUserInfo = true;
      wx.setStorageSync('token', token);
      wx.setStorageSync('userInfo', userInfo);
    } else {
      this.globalData.token = null;
      this.globalData.userInfo = null;
      this.globalData.hasUserInfo = false;
      wx.removeStorageSync('token');
      wx.removeStorageSync('userInfo');
    }
  }
})

最主要的就是

js 复制代码
    wx.setStorageSync('token', token);
    wx.setStorageSync('userInfo', userInfo);

使用的时候就这样:

js 复制代码
const app = getApp();

// 1. 获取全局数据
const { userInfo, token } = app.globalData;

// 2. 调用全局方法
app.checkLoginStatus();
app.updateLoginStatus(token, userInfo);

在代码里面一般这样用,在进入获取用户信息的时候用app.checkLoginStatus(),在登录的时候用:app.updateLoginStatus(token, completeUserInfo);

js 复制代码
// 获取用户信息时
getUserInfo() {
  // 从全局状态获取登录信息
  const { hasUserInfo, userInfo } = app.globalData;
  
  if (hasUserInfo && userInfo) {
    // 使用全局数据更新页面
    this.setData({
      userInfo,
      hasUserInfo: true
    });
  } else {
    // 检查登录状态
    const isLoggedIn = app.checkLoginStatus();
    // ...
  }
}

// 登录成功时
// 6. 更新全局状态
app.updateLoginStatus(token, completeUserInfo);

4.2.2请求带上token

缓存本地token能干嘛?就是在请求后端接口的时候,从缓存里面拿到token,然后放到请求里面去

js 复制代码
const token = wx.getStorageSync('token');
wx.request({
  url: 'api/xxx',
  header: {
    'Authorization': `Bearer ${token}`
  }
})

一般情况下,我们会在小程序项目里面有个请求拦截文件,进行统一处理,所以你在请求api的时候一般看不到,如下:

4.2.3后端解析token,验证身份

当后端接收到请求以后,它会用jwt实例去解析token,token其实就是userId和openid的加密字符串。

js 复制代码
// 当收到请求时,后端会:
try {
  // 1. 从请求头获取 token
  const token = ctx.header.authorization?.split(' ')[1];
  
  // 2. 使用相同的密钥验证 token
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  
  // 3. 如果验证成功,可以获取到用户信息
  const userId = decoded.userId;
  const openId = decoded.openId;
  
  // 4. 验证失败会抛出异常
} catch (err) {
  // token 无效或已过期
  ctx.throw(401, '无效的token');
}

如果你想知道token里面到底是什么就用免费的JWT解析器解析一下看看

4.2.4.退出登录处理

前端微信删除token,然后后端会更新用户登录状态

js 复制代码
// 前端清除本地存储的token
wx.removeStorageSync('token')

// 后端更新用户状态
await User.update({
  updateTime: new Date()
}, {
  where: { userId }
});

5.利用心跳包来维护登录状态

5.1概念

心跳包就是前端有个接口,定时的向后端发送请求,让服务器告诉我,我还活着没有

js 复制代码
// 心跳包就像人的心跳一样,定期发送一个信号,表示"我还活着"
// 例如:每30秒发送一次请求
setInterval(() => {
  wx.request({
    url: '/api/heartbeat',
    method: 'POST',
    success: () => {
      console.log('心跳包发送成功');
    }
  });
}, 30000); // 30秒

具体的实现如下:

前端

js 复制代码
// 在小程序中实现心跳机制
const startHeartbeat = () => {
  // 1. 首次连接
  sendHeartbeat();
  
  // 2. 定期发送
  const intervalId = setInterval(() => {
    sendHeartbeat();
  }, 30000);
  
  // 3. 保存定时器ID,以便可以停止
  wx.setStorageSync('heartbeatInterval', intervalId);
};

const sendHeartbeat = async () => {
  try {
    const res = await wx.request({
      url: 'api/user/heartbeat',
      method: 'POST',
      header: {
        'Authorization': `Bearer ${wx.getStorageSync('token')}`
      }
    });
    
    if (res.statusCode === 200) {
      console.log('心跳成功');
    } else {
      console.log('心跳失败,可能需要重新登录');
    }
  } catch (error) {
    console.error('心跳请求失败:', error);
  }
};

// 在用户登录时启动心跳
onLogin() {
  startHeartbeat();
}

// 在用户退出时停止心跳
onLogout() {
  const intervalId = wx.getStorageSync('heartbeatInterval');
  if (intervalId) {
    clearInterval(intervalId);
  }
}

后端

js 复制代码
// 在Redis中记录用户在线状态
class UserController {
  static async heartbeat(ctx) {
    const userId = ctx.state.user.userId;
    
    // 使用Redis记录用户在线状态
    await redis.setex(
      `user:online:${userId}`,  // key
      60,                       // 过期时间(秒)
      'online'                  // value
    );
    
    ctx.body = {
      code: 200,
      message: 'Heartbeat received'
    };
  }
  
  // 检查用户是否在线
  static async checkUserOnline(userId) {
    const status = await redis.get(`user:online:${userId}`);
    return status === 'online';
  }
}

这个机制的好处是:

  1. 实时性:可以及时发现用户离线
  1. 可靠性:通过定期检查确保连接状态
  1. 灵活性:可以根据需求调整心跳频率
  1. 安全性:可以及时清理过期的会话

缺点是:

  1. 需要额外的服务器资源
  1. 增加了网络流量
  1. 需要考虑移动设备的电量消耗
  1. 需要处理网络波动的情况

您觉得需要在项目中添加这个功能吗?我可以帮您实现。

这个登录认证流程的优点是:

  1. 使用微信官方的登录机制,安全可靠
  1. 采用 JWT 进行身份验证,无需保存会话状态
  1. token 有过期机制,增加安全性
  1. 支持自动创建新用户和更新用户信息
  1. 完整的错误处理和日志记录
相关推荐
czliutz12 分钟前
NiceGUI 是一个基于 Python 的现代 Web 应用框架
开发语言·前端·python
koooo~2 小时前
【无标题】
前端
Attacking-Coder2 小时前
前端面试宝典---前端水印
前端
linweidong3 小时前
Go开发简历优化指南
分布式·后端·golang·高并发·简历优化·go面试·后端面经
咖啡啡不加糖4 小时前
雪花算法:分布式ID生成的优雅解决方案
java·分布式·后端
姑苏洛言5 小时前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
烛阴5 小时前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
why1515 小时前
字节golang后端二面
开发语言·后端·golang
还是鼠鼠5 小时前
单元测试-断言&常见注解
java·开发语言·后端·单元测试·maven
Alice_hhu5 小时前
ResizeObserver 解决 echarts渲染不出来,内容宽度为 0的问题
前端·javascript·echarts