最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Express實(shí)現(xiàn)微信登錄的雙token機(jī)制的項(xiàng)目實(shí)踐

 更新時(shí)間:2025年08月08日 10:37:22   作者:實(shí)習(xí)生小黃  
本文主要介紹了Express實(shí)現(xiàn)微信登錄的雙token機(jī)制,兼容微信小程序與傳統(tǒng)賬號(hào)密碼登錄,通過(guò)訪問(wèn)令牌和刷新令牌機(jī)制,解決Token過(guò)期問(wèn)題,優(yōu)化用戶體驗(yàn)并提升安全性

前言

之前在學(xué)習(xí) Express 的過(guò)程當(dāng)中,稍微去了解過(guò)服務(wù)端的登錄流程,但是最近在和朋友開(kāi)發(fā)一款小程序,小程序的登錄方式又不同于以往的賬號(hào)密碼登錄,并且想要將之前的登錄優(yōu)化為雙token的模式,優(yōu)化用戶體驗(yàn)。所以就兼容了兩種登錄方式,并且添加了 雙token 認(rèn)證的優(yōu)化方式。

什么是Express

Express是基于Node.js的Web應(yīng)用框架,提供了一系列強(qiáng)大的特性來(lái)幫助開(kāi)發(fā)者創(chuàng)建各種Web應(yīng)用。它簡(jiǎn)潔而靈活,是目前最流行的Node.js服務(wù)器框架之一。

Express的主要特點(diǎn)包括:

  1. 中間件系統(tǒng):允許開(kāi)發(fā)者創(chuàng)建請(qǐng)求處理管道
  2. 路由系統(tǒng):簡(jiǎn)化URL到處理函數(shù)的映射
  3. 模板引擎集成:支持多種模板引擎
  4. 錯(cuò)誤處理機(jī)制:提供統(tǒng)一的錯(cuò)誤處理方式
  5. 靜態(tài)文件服務(wù):輕松提供靜態(tài)資源

想要使用 Express 實(shí)現(xiàn)一個(gè)服務(wù)非常的簡(jiǎn)單:

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機(jī)制,它用來(lái)解決什么問(wèn)題

雙token機(jī)制概述

雙token認(rèn)證機(jī)制,也稱(chēng)為刷新令牌模式,包含兩種類(lèi)型的token:

  1. 訪問(wèn)令牌(Access Token):短期有效,用于API訪問(wèn)認(rèn)證
  2. 刷新令牌(Refresh Token):長(zhǎng)期有效,用于獲取新的訪問(wèn)令牌

主要的業(yè)務(wù)流程就是客戶端在登錄成功之后返回 Access TokenRefresh Token,在 Access Token 過(guò)期之后,會(huì)調(diào)用接口使用 Refresh Token重新獲取 Access Token。并且在刷新的時(shí)候會(huì)同步重置 Refresh Token 的時(shí)效,也就是如果在 Refresh Token 有效期內(nèi)一直有使用記錄,就可以不斷地刷新,本質(zhì)上可以優(yōu)化一些經(jīng)常使用程序的用戶體驗(yàn),而對(duì)于長(zhǎng)時(shí)間未使用的用戶(超過(guò)了 Refresh Token 的有效期),就需要重新登錄。

雙token的實(shí)現(xiàn)示例

以下是生成雙token的核心函數(shù):

// 生成雙token的輔助函數(shù)
function generateTokens(userId, additionalData = {}) {
  const payload = { userId, ...additionalData };
  
  // 生成access_token,1小時(shí)過(guò)期
  const accessToken = jwt.sign(
    payload, 
    process.env.JWT_SECRET || "xxx-your-secret-key", 
    { expiresIn: "1h" }
  );
  
  // 生成refresh_token,7天過(guò)期
  const refreshToken = jwt.sign(
    payload, 
    process.env.JWT_REFRESH_SECRET || "xxx-your-refresh-secret-key", 
    { expiresIn: "7d" }
  );
  
  return { accessToken, refreshToken };
}

微信小程序的登錄流程

關(guān)于雙token的一個(gè)業(yè)務(wù)流程,下面用一張圖來(lái)展示一下

微信小程序的登錄流程與傳統(tǒng)Web應(yīng)用有所不同,主要包括以下步驟:

  1. 前端獲取登錄憑證(code)

    • 小程序調(diào)用wx.login()獲取臨時(shí)登錄憑證code
    • code有效期為5分鐘,只能使用一次
  2. 后端換取openid和session_key

    • 服務(wù)端調(diào)用微信接口,使用appid、secret和code獲取openid和session_key
    • openid是用戶在該小程序的唯一標(biāo)識(shí)
    • session_key用于解密用戶信息
  3. 生成自定義登錄態(tài)

    • 服務(wù)端生成自定義登錄態(tài)(如JWT token)
    • 將openid與用戶信息關(guān)聯(lián)存儲(chǔ)
  4. 維護(hù)登錄態(tài)

    • 小程序存儲(chǔ)登錄態(tài),后續(xù)請(qǐng)求攜帶
    • 服務(wù)端驗(yàn)證登錄態(tài)有效性

下面是微信登錄的服務(wù)端實(shí)現(xiàn):

// 微信認(rèn)證中間件
async function wxLogin(code) {
  try {
    // 使用環(huán)境變量中的微信配置
    const appid = process.env.APP_ID || process.env.APP_ID;
    const secret = process.env.APP_SECRET || process.env.APP_SECRET;

    // 調(diào)用微信接口獲取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;
  }
}

服務(wù)端如何兼容微信小程序登錄和賬號(hào)密碼登錄

統(tǒng)一的用戶模型設(shè)計(jì)

首先,我們需要設(shè)計(jì)一個(gè)統(tǒng)一的用戶模型,既能支持傳統(tǒng)賬號(hào)密碼,又能關(guān)聯(lián)微信openid:

// User模型定義
User.init(
  {
    userName: {
      comment: "用戶名",
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
    },
    password: {
      comment: "密碼",
      type: DataTypes.STRING,
      allowNull: true, // 允許為空,因?yàn)槲⑿诺卿洸恍枰艽a
    },
    email: {
      comment: "郵箱",
      type: DataTypes.STRING,
      allowNull: true, // 允許為空,因?yàn)槲⑿诺卿浛赡軟](méi)有郵箱
    },
    openid: {
      comment: "微信openid",
      type: DataTypes.STRING,
      allowNull: true,
      unique: true,
    },
    img: {
      comment: "微信頭像URL",
      type: DataTypes.STRING,
      allowNull: true,
    },
    lastOnlineTime: {
      comment: "最后登陸時(shí)間",
      type: DataTypes.DATE,
      allowNull: true,
    },
    refreshToken: {
      comment: "刷新令牌",
      type: DataTypes.TEXT,
      allowNull: true,
    },
  },
  {
    sequelize,
    modelName: "User",
  }
);

實(shí)現(xiàn)賬號(hào)密碼登錄

傳統(tǒng)的賬號(hào)密碼登錄流程:

// 傳統(tǒng)用戶名密碼登錄
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" });
    }

    // 更新用戶的最后在線時(shí)間
    user.lastOnlineTime = new Date();
    await user.save();

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

    // 保存refresh_token到數(shù)據(jù)庫(kù)
    user.refreshToken = refreshToken;
    await user.save();

    // 返回包含雙token的響應(yīng)
    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" });
  }
}

實(shí)現(xiàn)微信登錄

微信小程序登錄流程:

// 微信登錄
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" });
    }
    
    // 查找或創(chuàng)建用戶
    let user = await User.findOne({ where: { openid } });
    
    if (!user) {
      // 如果用戶不存在,創(chuàng)建新用戶
      user = await User.create({
        userName: `wx_user_${openid.substring(0, 8)}`, // 生成一個(gè)基于openid的用戶名
        openid,
        lastOnlineTime: new Date(),
      });
    } else {
      // 更新用戶的最后在線時(shí)間
      user.lastOnlineTime = new Date();
    }

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

    // 保存refresh_token到數(shù)據(jù)庫(kù)
    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" });
  }
}

實(shí)現(xiàn)token刷新

當(dāng)access_token過(guò)期時(shí),客戶端可以使用refresh_token獲取新的token對(duì):

// 刷新token
async function refreshToken(req, res) {
  try {
    const user = req.userData; // 從中間件獲取用戶數(shù)據(jù)
    
    // 生成新的雙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);
    
    // 更新數(shù)據(jù)庫(kù)中的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" });
  }
}

驗(yàn)證中間件

為了保護(hù)API路由,我們需要兩個(gè)中間件:一個(gè)驗(yàn)證access_token,另一個(gè)驗(yàn)證refresh_token:

1. 驗(yàn)證access_token的中間件:

// 鑒權(quán)中間件 - 只驗(yàn)證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"
    });
  }

  // 驗(yàn)證 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"
      });
    }

    // 將用戶信息存儲(chǔ)到請(qǐng)求對(duì)象中
    req.user = user;
    next();
  });
}

2. 驗(yàn)證refresh_token的中間件:

// 驗(yàn)證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 {
    // 驗(yàn)證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"
      });
    }
    
    // 檢查數(shù)據(jù)庫(kù)中的refresh_token是否匹配
    if (user.refreshToken !== refreshToken) {
      return res.status(401).json({ 
        error: "Invalid refresh token",
        code: "INVALID_REFRESH_TOKEN"
      });
    }
    
    // 將用戶信息存儲(chǔ)到請(qǐng)求對(duì)象中
    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刷新集成到一起:

// 不需要認(rèn)證的路由
// 注冊(cè)
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);

// 需要認(rèn)證的路由
router.post('/logout', authMiddleware, authController.logout);
// 用戶信息 CRUD
router.get('/user-info', authMiddleware, authController.getUserInfo);
router.put('/user-info', authMiddleware, authController.updateUserInfo);

測(cè)試

微信登錄

刷新token

總結(jié)

本文介紹了用 Express框架中實(shí)現(xiàn)雙token認(rèn)證機(jī)制,并且在基礎(chǔ)的賬號(hào)密碼登錄上支持了 微信小程序登錄。這種方案不僅是簡(jiǎn)化了用戶的登錄流程,也優(yōu)化了用戶的使用體驗(yàn),并且在安全性上也能有所提升。

到此這篇關(guān)于Express實(shí)現(xiàn)微信登錄的雙token機(jī)制的文章就介紹到這了,更多相關(guān)Express微信登錄雙token內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Node的stream數(shù)據(jù)流你了解嗎

    Node的stream數(shù)據(jù)流你了解嗎

    這篇文章主要為大家詳細(xì)介紹了Node的stream數(shù)據(jù)流,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2022-02-02
  • 使用NodeJS?5分鐘?連接?Redis?讀寫(xiě)操作的詳細(xì)過(guò)程

    使用NodeJS?5分鐘?連接?Redis?讀寫(xiě)操作的詳細(xì)過(guò)程

    這篇文章主要介紹了NodeJS?5分鐘?連接?Redis?讀寫(xiě)操作,本文給大家介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • node.js安裝及HbuilderX配置詳解

    node.js安裝及HbuilderX配置詳解

    這篇文章主要介紹了node.js安裝及HbuilderX配置的相關(guān)資料,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-03-03
  • 基于Node.js的http模塊搭建HTTP服務(wù)器

    基于Node.js的http模塊搭建HTTP服務(wù)器

    這篇文章主要為大家介紹了基于Node.js的http模塊來(lái)搭建HTTP服務(wù)器的示例過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步
    2022-02-02
  • node.js之基礎(chǔ)加密算法模塊crypto詳解

    node.js之基礎(chǔ)加密算法模塊crypto詳解

    這篇文章主要介紹了node.js之基礎(chǔ)加密算法模塊crypto詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-09-09
  • Nodejs 識(shí)別圖片類(lèi)型的方法

    Nodejs 識(shí)別圖片類(lèi)型的方法

    這篇文章主要介紹了Nodejs 識(shí)別圖片類(lèi)型的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • 詳解NodeJs項(xiàng)目 CentOs linux服務(wù)器線上部署

    詳解NodeJs項(xiàng)目 CentOs linux服務(wù)器線上部署

    這篇文章主要介紹了NodeJs項(xiàng)目 CentOs linux服務(wù)器線上部署,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-09-09
  • Express使用multer實(shí)現(xiàn)文件上傳的示例代碼

    Express使用multer實(shí)現(xiàn)文件上傳的示例代碼

    這篇文章主要介紹了Express 使用 multer 實(shí)現(xiàn)文件上傳的操作步驟,文中通過(guò)代碼示例和圖文結(jié)合的方式講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-03-03
  • nodejs發(fā)布靜態(tài)https服務(wù)器的方法

    nodejs發(fā)布靜態(tài)https服務(wù)器的方法

    這篇文章主要介紹了nodejs發(fā)布靜態(tài)https服務(wù)器的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-09-09
  • Node server生成Swagger接口文檔步驟詳解

    Node server生成Swagger接口文檔步驟詳解

    本文介紹使用NodeJs搭建的后端server快速生成Swagger接口文檔的技巧,本文有詳細(xì)的步驟講解,通過(guò)代碼示例給大家介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下
    2024-01-01

最新評(píng)論

随州市| 华亭县| 左贡县| 区。| 遵义市| 方正县| 揭西县| 扬中市| 沙洋县| 新源县| 无为县| 永德县| 玉树县| 开封县| 大厂| 孟连| 黄石市| 吐鲁番市| 天等县| 沈丘县| 铜川市| 保山市| 太谷县| 修武县| 都兰县| 菏泽市| 宜城市| 松阳县| 宣化县| 拉萨市| 高尔夫| 和顺县| 全州县| 泰和县| 东至县| 红桥区| 湖口县| 栾川县| 济源市| 瓮安县| 巧家县|