Express實現(xiàn)微信登錄的雙token機制的項目實踐
前言
之前在學習 Express 的過程當中,稍微去了解過服務(wù)端的登錄流程,但是最近在和朋友開發(fā)一款小程序,小程序的登錄方式又不同于以往的賬號密碼登錄,并且想要將之前的登錄優(yōu)化為雙token的模式,優(yōu)化用戶體驗。所以就兼容了兩種登錄方式,并且添加了 雙token 認證的優(yōu)化方式。
什么是Express
Express是基于Node.js的Web應(yīng)用框架,提供了一系列強大的特性來幫助開發(fā)者創(chuàng)建各種Web應(yīng)用。它簡潔而靈活,是目前最流行的Node.js服務(wù)器框架之一。
Express的主要特點包括:
- 中間件系統(tǒng):允許開發(fā)者創(chuàng)建請求處理管道
- 路由系統(tǒng):簡化URL到處理函數(shù)的映射
- 模板引擎集成:支持多種模板引擎
- 錯誤處理機制:提供統(tǒng)一的錯誤處理方式
- 靜態(tài)文件服務(wù):輕松提供靜態(tài)資源
想要使用 Express 實現(xiàn)一個服務(wù)非常的簡單:
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:
- 訪問令牌(Access Token):短期有效,用于API訪問認證
- 刷新令牌(Refresh Token):長期有效,用于獲取新的訪問令牌
主要的業(yè)務(wù)流程就是客戶端在登錄成功之后返回 Access Token 和 Refresh Token,在 Access Token 過期之后,會調(diào)用接口使用 Refresh Token重新獲取 Access Token。并且在刷新的時候會同步重置 Refresh Token 的時效,也就是如果在 Refresh Token 有效期內(nèi)一直有使用記錄,就可以不斷地刷新,本質(zhì)上可以優(yōu)化一些經(jīng)常使用程序的用戶體驗,而對于長時間未使用的用戶(超過了 Refresh Token 的有效期),就需要重新登錄。
雙token的實現(xiàn)示例
以下是生成雙token的核心函數(shù):
// 生成雙token的輔助函數(shù)
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 };
}
微信小程序的登錄流程
關(guān)于雙token的一個業(yè)務(wù)流程,下面用一張圖來展示一下

微信小程序的登錄流程與傳統(tǒng)Web應(yīng)用有所不同,主要包括以下步驟:
前端獲取登錄憑證(code):
- 小程序調(diào)用
wx.login()獲取臨時登錄憑證code - code有效期為5分鐘,只能使用一次
- 小程序調(diào)用
后端換取openid和session_key:
- 服務(wù)端調(diào)用微信接口,使用appid、secret和code獲取openid和session_key
- openid是用戶在該小程序的唯一標識
- session_key用于解密用戶信息
生成自定義登錄態(tài):
- 服務(wù)端生成自定義登錄態(tài)(如JWT token)
- 將openid與用戶信息關(guān)聯(lián)存儲
維護登錄態(tài):
- 小程序存儲登錄態(tài),后續(xù)請求攜帶
- 服務(wù)端驗證登錄態(tài)有效性
下面是微信登錄的服務(wù)端實現(xià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ù)端如何兼容微信小程序登錄和賬號密碼登錄
統(tǒng)一的用戶模型設(shè)計
首先,我們需要設(shè)計一個統(tǒng)一的用戶模型,既能支持傳統(tǒng)賬號密碼,又能關(guān)聯(lián)微信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",
}
);
實現(xiàn)賬號密碼登錄
傳統(tǒng)的賬號密碼登錄流程:
// 傳統(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" });
}
// 更新用戶的最后在線時間
user.lastOnlineTime = new Date();
await user.save();
// 生成雙token
const { accessToken, refreshToken } = generateTokens(user.id);
// 保存refresh_token到數(shù)據(jù)庫
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" });
}
}
實現(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)}`, // 生成一個基于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到數(shù)據(jù)庫
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" });
}
}
實現(xiàn)token刷新
當access_token過期時,客戶端可以使用refresh_token獲取新的token對:
// 刷新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ù)庫中的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的中間件:
// 鑒權(quá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"
});
}
// 驗證 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"
});
}
// 檢查數(shù)據(jù)庫中的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

總結(jié)
本文介紹了用 Express框架中實現(xiàn)雙token認證機制,并且在基礎(chǔ)的賬號密碼登錄上支持了 微信小程序登錄。這種方案不僅是簡化了用戶的登錄流程,也優(yōu)化了用戶的使用體驗,并且在安全性上也能有所提升。
到此這篇關(guān)于Express實現(xiàn)微信登錄的雙token機制的文章就介紹到這了,更多相關(guān)Express微信登錄雙token內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Nodejs進階:express+session實現(xiàn)簡易登錄身份認證
- Node.js+Express+MySql實現(xiàn)用戶登錄注冊功能
- Node+Express+MongoDB實現(xiàn)登錄注冊功能實例
- Express + Session 實現(xiàn)登錄驗證功能
- Express + Node.js實現(xiàn)登錄攔截器的實例代碼
- Vue+Express實現(xiàn)登錄注銷功能的實例代碼
- vue+express+jwt持久化登錄的方法
- express + jwt + postMan驗證實現(xiàn)持久化登錄
- Vue+Express實現(xiàn)登錄狀態(tài)權(quán)限驗證的示例代碼
相關(guān)文章
使用NodeJS?5分鐘?連接?Redis?讀寫操作的詳細過程
這篇文章主要介紹了NodeJS?5分鐘?連接?Redis?讀寫操作,本文給大家介紹的非常詳細,對大家學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
詳解NodeJs項目 CentOs linux服務(wù)器線上部署
這篇文章主要介紹了NodeJs項目 CentOs linux服務(wù)器線上部署,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-09-09
Express使用multer實現(xiàn)文件上傳的示例代碼
這篇文章主要介紹了Express 使用 multer 實現(xiàn)文件上傳的操作步驟,文中通過代碼示例和圖文結(jié)合的方式講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-03-03
nodejs發(fā)布靜態(tài)https服務(wù)器的方法
這篇文章主要介紹了nodejs發(fā)布靜態(tài)https服務(wù)器的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09

