node.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > node.js > Express微信登录双token

Express实现微信登录的双token机制的项目实践

作者:实习生小黄

本文主要介绍了Express实现微信登录的双token机制,兼容微信小程序与传统账号密码登录,通过访问令牌和刷新令牌机制,解决Token过期问题,优化用户体验并提升安全性

前言

之前在学习 Express 的过程当中,稍微去了解过服务端的登录流程,但是最近在和朋友开发一款小程序,小程序的登录方式又不同于以往的账号密码登录,并且想要将之前的登录优化为双token的模式,优化用户体验。所以就兼容了两种登录方式,并且添加了 双token 认证的优化方式。

什么是Express

Express是基于Node.js的Web应用框架,提供了一系列强大的特性来帮助开发者创建各种Web应用。它简洁而灵活,是目前最流行的Node.js服务器框架之一。

Express的主要特点包括:

  1. 中间件系统:允许开发者创建请求处理管道
  2. 路由系统:简化URL到处理函数的映射
  3. 模板引擎集成:支持多种模板引擎
  4. 错误处理机制:提供统一的错误处理方式
  5. 静态文件服务:轻松提供静态资源

想要使用 Express 实现一个服务非常的简单:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

什么是双token机制,它用来解决什么问题

双token机制概述

双token认证机制,也称为刷新令牌模式,包含两种类型的token:

  1. 访问令牌(Access Token):短期有效,用于API访问认证
  2. 刷新令牌(Refresh Token):长期有效,用于获取新的访问令牌

主要的业务流程就是客户端在登录成功之后返回 Access TokenRefresh Token,在 Access Token 过期之后,会调用接口使用 Refresh Token重新获取 Access Token。并且在刷新的时候会同步重置 Refresh Token 的时效,也就是如果在 Refresh Token 有效期内一直有使用记录,就可以不断地刷新,本质上可以优化一些经常使用程序的用户体验,而对于长时间未使用的用户(超过了 Refresh Token 的有效期),就需要重新登录。

双token的实现示例

以下是生成双token的核心函数:

// 生成双token的辅助函数
function generateTokens(userId, additionalData = {}) {
  const payload = { userId, ...additionalData };
  
  // 生成access_token,1小时过期
  const accessToken = jwt.sign(
    payload, 
    process.env.JWT_SECRET || "xxx-your-secret-key", 
    { expiresIn: "1h" }
  );
  
  // 生成refresh_token,7天过期
  const refreshToken = jwt.sign(
    payload, 
    process.env.JWT_REFRESH_SECRET || "xxx-your-refresh-secret-key", 
    { expiresIn: "7d" }
  );
  
  return { accessToken, refreshToken };
}

微信小程序的登录流程

关于双token的一个业务流程,下面用一张图来展示一下

微信小程序的登录流程与传统Web应用有所不同,主要包括以下步骤:

  1. 前端获取登录凭证(code)

    • 小程序调用wx.login()获取临时登录凭证code
    • code有效期为5分钟,只能使用一次
  2. 后端换取openid和session_key

    • 服务端调用微信接口,使用appid、secret和code获取openid和session_key
    • openid是用户在该小程序的唯一标识
    • session_key用于解密用户信息
  3. 生成自定义登录态

    • 服务端生成自定义登录态(如JWT token)
    • 将openid与用户信息关联存储
  4. 维护登录态

    • 小程序存储登录态,后续请求携带
    • 服务端验证登录态有效性

下面是微信登录的服务端实现:

// 微信认证中间件
async function wxLogin(code) {
  try {
    // 使用环境变量中的微信配置
    const appid = process.env.APP_ID || process.env.APP_ID;
    const secret = process.env.APP_SECRET || process.env.APP_SECRET;

    // 调用微信接口获取openid和session_key
    const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
      params: {
        appid,
        secret,
        js_code: code,
        grant_type: 'authorization_code',
      },
    });

    const { openid, session_key, errcode, errmsg } = response.data;

    if (errcode) {
      throw new Error(`WeChat API error: ${errcode}, ${errmsg}`);
    }

    return { openid, session_key };
  } catch (error) {
    console.error('WeChat authentication error:', error);
    throw error;
  }
}

服务端如何兼容微信小程序登录和账号密码登录

统一的用户模型设计

首先,我们需要设计一个统一的用户模型,既能支持传统账号密码,又能关联微信openid:

// User模型定义
User.init(
  {
    userName: {
      comment: "用户名",
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
    },
    password: {
      comment: "密码",
      type: DataTypes.STRING,
      allowNull: true, // 允许为空,因为微信登录不需要密码
    },
    email: {
      comment: "邮箱",
      type: DataTypes.STRING,
      allowNull: true, // 允许为空,因为微信登录可能没有邮箱
    },
    openid: {
      comment: "微信openid",
      type: DataTypes.STRING,
      allowNull: true,
      unique: true,
    },
    img: {
      comment: "微信头像URL",
      type: DataTypes.STRING,
      allowNull: true,
    },
    lastOnlineTime: {
      comment: "最后登陆时间",
      type: DataTypes.DATE,
      allowNull: true,
    },
    refreshToken: {
      comment: "刷新令牌",
      type: DataTypes.TEXT,
      allowNull: true,
    },
  },
  {
    sequelize,
    modelName: "User",
  }
);

实现账号密码登录

传统的账号密码登录流程:

// 传统用户名密码登录
async function login(req, res) {
  const { userName, password } = req.body;

  try {
    // 检查用户名是否存在
    const user = await User.findOne({ where: { userName } });
    if (!user) {
      return res.status(401).json({ msg: "Invalid userName or password" });
    }

    // 检查密码是否匹配
    const isPasswordMatch = await bcrypt.compare(password, user.password);
    if (!isPasswordMatch) {
      return res.status(401).json({ msg: "Invalid userName or password" });
    }

    // 更新用户的最后在线时间
    user.lastOnlineTime = new Date();
    await user.save();

    // 生成双token
    const { accessToken, refreshToken } = generateTokens(user.id);

    // 保存refresh_token到数据库
    user.refreshToken = refreshToken;
    await user.save();

    // 返回包含双token的响应
    res.json({
      accessToken,
      refreshToken,
      account: user.userName,
      email: user.email,
      userId: user.id,
    });
  } catch (error) {
    console.log("🚀 ~ login ~ error:", error);
    res.status(500).json({ msg: "Failed to log in" });
  }
}

实现微信登录

微信小程序登录流程:

// 微信登录
async function wxLoginHandler(req, res) {
  const { code } = req.body;
  
  if (!code) {
    return res.status(400).json({ msg: "WeChat code is required" });
  }

  try {
    // 获取微信openid和session_key
    const { openid, session_key } = await wxLogin(code);
    
    if (!openid) {
      return res.status(400).json({ msg: "Failed to get WeChat openid" });
    }
    
    // 查找或创建用户
    let user = await User.findOne({ where: { openid } });
    
    if (!user) {
      // 如果用户不存在,创建新用户
      user = await User.create({
        userName: `wx_user_${openid.substring(0, 8)}`, // 生成一个基于openid的用户名
        openid,
        lastOnlineTime: new Date(),
      });
    } else {
      // 更新用户的最后在线时间
      user.lastOnlineTime = new Date();
    }

    // 生成双token,包含openid和session_key
    const { accessToken, refreshToken } = generateTokens(user.id, {
      openid,
      session_key,
    });

    // 保存refresh_token到数据库
    user.refreshToken = refreshToken;
    await user.save();

    // 返回用户信息和双token
    res.json({
      accessToken,
      refreshToken,
      userId: user.id,
      userName: user.userName,
      img: user.img,
      openid,
    });
  } catch (error) {
    console.log("🚀 ~ wxLoginHandler ~ error:", error);
    res.status(500).json({ msg: "Failed to login with WeChat" });
  }
}

实现token刷新

当access_token过期时,客户端可以使用refresh_token获取新的token对:

// 刷新token
async function refreshToken(req, res) {
  try {
    const user = req.userData; // 从中间件获取用户数据
    
    // 生成新的双token
    const additionalData = {};
    if (req.user.openid) {
      additionalData.openid = req.user.openid;
      additionalData.session_key = req.user.session_key;
    }
    
    const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
      generateTokens(user.id, additionalData);
    
    // 更新数据库中的refresh_token
    user.refreshToken = newRefreshToken;
    await user.save();
    
    // 返回新的双token
    res.json({
      accessToken: newAccessToken,
      refreshToken: newRefreshToken,
    });
  } catch (error) {
    console.log("🚀 ~ refreshToken ~ error:", error);
    res.status(500).json({ msg: "Failed to refresh token" });
  }
}

验证中间件

为了保护API路由,我们需要两个中间件:一个验证access_token,另一个验证refresh_token:

1. 验证access_token的中间件:

// 鉴权中间件 - 只验证access_token
function authMiddleware(req, res, next) {
  const authHeader = req.headers["authorization"];
  // 从 Authorization 头部解析 token
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ 
      error: "Access token is required",
      code: "MISSING_TOKEN"
    });
  }

  // 验证 access_token
  jwt.verify(token, process.env.JWT_SECRET || "xxx-your-secret-key", (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ 
          error: "Access token expired", 
          code: "TOKEN_EXPIRED"
        });
      }
      
      return res.status(403).json({ 
        error: "Invalid access token",
        code: "INVALID_TOKEN"
      });
    }

    // 将用户信息存储到请求对象中
    req.user = user;
    next();
  });
}

2. 验证refresh_token的中间件:

// 验证refresh_token的中间件
async function refreshTokenMiddleware(req, res, next) {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ 
      error: "Refresh token is required",
      code: "MISSING_REFRESH_TOKEN"
    });
  }

  try {
    // 验证refresh_token
    const decoded = jwt.verify(
      refreshToken, 
      process.env.JWT_REFRESH_SECRET || "xxx-your-refresh-secret-key"
    );
    
    // 查找用户
    const user = await User.findByPk(decoded.userId);
    if (!user) {
      return res.status(401).json({ 
        error: "User not found",
        code: "USER_NOT_FOUND"
      });
    }
    
    // 检查数据库中的refresh_token是否匹配
    if (user.refreshToken !== refreshToken) {
      return res.status(401).json({ 
        error: "Invalid refresh token",
        code: "INVALID_REFRESH_TOKEN"
      });
    }
    
    // 将用户信息存储到请求对象中
    req.user = decoded;
    req.userData = user;
    
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: "Refresh token expired",
        code: "REFRESH_TOKEN_EXPIRED"
      });
    }
    
    return res.status(401).json({ 
      error: "Invalid refresh token",
      code: "INVALID_REFRESH_TOKEN"
    });
  }
}

路由配置

最后,我们需要配置路由,将不同的登录方式和token刷新集成到一起:

// 不需要认证的路由
// 注册
router.post('/register', authController.register);
// 登录
router.post('/login', authController.login);
// 微信登录
router.post('/wx-login', authController.wxLoginHandler); 

// 使用refresh token中间件的路由
router.post('/refresh-token', refreshTokenMiddleware, authController.refreshToken);

// 需要认证的路由
router.post('/logout', authMiddleware, authController.logout);
// 用户信息 CRUD
router.get('/user-info', authMiddleware, authController.getUserInfo);
router.put('/user-info', authMiddleware, authController.updateUserInfo);

测试

微信登录

刷新token

总结

本文介绍了用 Express框架中实现双token认证机制,并且在基础的账号密码登录上支持了 微信小程序登录。这种方案不仅是简化了用户的登录流程,也优化了用户的使用体验,并且在安全性上也能有所提升。

到此这篇关于Express实现微信登录的双token机制的文章就介绍到这了,更多相关Express微信登录双token内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文