微信授权小程序的全过程

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. 完整的错误处理和日志记录
相关推荐
橙子家9 小时前
浏览器缓存之【基础键值存储】:Local storage 和 Session storage
前端
星星在线11 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒12 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x12 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
京东云开发者13 小时前
京东市民服务又“上新”!这次是黑龙江“龙易办”
前端
袋鱼不重14 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户83562907805114 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还14 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
Hommy8814 小时前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api
Fireworks14 小时前
深入vue3源码解读 -- 1、响应式的基础概念
前端