本文详细阐述了如何在node.js应用中,利用jwt(json web tokens)和cookie实现持久化的用户登录状态管理,从而避免用户在每次访问时重复登录。通过引入一个认证中间件,我们能够有效地检查用户是否已通过有效令牌进行身份验证,并据此控制页面访问权限,实现无缝的用户体验,同时提供了登出功能和安全实践建议。
在现代Web应用中,用户体验至关重要。频繁要求用户重复登录会极大地降低用户满意度。为了解决这一问题,我们需要一种机制来持久化用户的登录状态,即在用户首次登录后,即使关闭浏览器或离开网站,再次访问时也能保持登录状态,无需重新输入凭据。在Node.js环境中,结合JWT(JSON Web Tokens)和HTTP Cookie是实现这一目标的常用且高效的方法。
在提供的代码中,我们已经看到了一个基本的认证流程,它涵盖了用户注册、登录、密码哈希、JWT生成以及将JWT存储在Cookie中的步骤。
用户注册 (createUser):
用户登录 (loginUser):
JWT生成 (generateJWTToken 方法在 userModel 中):
这个流程成功地完成了用户认证和令牌的颁发。然而,当前的问题在于,尽管Cookie中存储了有效的JWT,但应用程序并没有在用户访问需要认证的页面(例如登录页面本身)之前检查这个Cookie,导致用户每次访问时都可能看到登录表单。
为了实现持久化登录状态和免登录访问,我们需要一个中间件函数。这个中间件会在请求到达特定路由之前执行,负责检查用户请求中是否存在有效的认证Cookie。如果存在且有效,就直接将用户重定向到已登录后的页面(例如主页);否则,才允许请求继续处理,例如渲染登录表单。
首先,创建一个名为 middleware.js 的文件,并在其中定义 isLoggedIn 函数。
// middleware.js
const jwt = require('jsonwebtoken');
module.exports.isLoggedIn = async (req, res, next) => {
try {
// 尝试从请求的cookies中获取名为'node1'的JWT
const token = req.cookies ? req.cookies.node1 : null;
// 如果存在token,则尝试验证其有效性
// "FULLSTACKWEBDEVELOPMENTMEANMERNBATCH" 必须与生成JWT时使用的密钥相同
const decoded = token ? jwt.verify(token, "FULLSTACKWEBDEVELOPMENTMEANMERNBATCH") : null;
// 如果JWT有效(即decoded不为null),说明用户已登录
if (decoded) {
// 将用户信息附加到请求对象,以便后续路由使用
// req.user = decoded.id; // 可以根据需要添加
// 直接重定向到用户主页,避免再次显示登录/注册页面
return res.redirect('/home');
}
// 如果没有有效的token,则用户未登录,允许请求继续到下一个中间件或路由处理器
next();
} catch (err) {
// 捕获JWT验证失败(如token过期、无效签名)或其他错误
// 在这种情况下,用户被视为未登录,可以重定向到登录页或允许渲染登录页
console.error("Authentication middleware error:", err.message);
// next(); // 也可以选择调用next(),让原始路由处理,例如渲染登录页
res.render("login"); // 或者直接渲染登录页
}
};中间件解析:
接下来,将这个中间件应用到需要控制访问的路由上,特别是那些在用户已登录时不应再次显示的页面,如登录页和注册页。
// routes.js (或你的主应用文件)
const express = require("express");
const router = express.Router();
const { isLoggedIn } = require("./middleware"); // 导入中间件
// ... 其他导入和配置
// 登录页面路由:如果用户已登录,isLoggedIn中间件会将其重定向到/home
router.get("/login", isLoggedIn, (req, res) => {
res.render("login"); // 如果isLoggedIn没有重定向,则渲染登录页
});
// 注册页面路由:同上
router.get("/reg", isLoggedIn, (req, res) => {
res.render("userRegister"); // 如果isLoggedIn没有重定向,则渲染注册页
});
// 用户登录API路由(保持不变,因为它处理表单提交,不直接渲染页面)
router.post("/api/userlogin", urlencodedParser, vUserControl
ler.loginUser);
// 假设有一个主页路由,用于显示登录后的内容
router.get("/home", (req, res) => {
// 这里可以添加一个保护,确保只有登录用户才能访问
// 例如:另一个中间件来验证所有受保护路由
res.render("home", { message: "欢迎回来!" });
});
// ... 其他路由通过这种方式,当用户尝试访问 /login 或 /reg 路由时:
为了提供完整的用户体验,还需要一个登出(Logout)功能。登出操作的核心是清除客户端存储的认证Cookie。
// routes.js (或你的主应用文件)
// ... 其他路由
// 登出路由
router.get('/logout', async (req, res) => {
// 清除名为 'node1' 的Cookie
res.clearCookie('node1');
// 重定向到登录页面
res.redirect("/login");
});当用户访问 /logout 路由时,res.clearCookie('node1') 会指示浏览器删除该Cookie,从而使用户的会话失效。随后,用户被重定向到登录页面。
JWT密钥安全: 用于签名和验证JWT的密钥("FULLSTACKWEBDEVELOPMENTMEANMERNBATCH")必须高度保密。在生产环境中,应将其存储在环境变量中,而不是硬编码在代码中。
Cookie安全:
JWT过期时间: 在 jwt.sign 时设置 expiresIn 选项,例如 expiresIn: '1h' 或 expiresIn: '7d',以限制令牌的有效期。过期后,用户需要重新登录。
保护所有受认证路由: isLoggedIn 中间件的当前实现主要用于防止已登录用户访问登录/注册页面。对于所有需要用户认证才能访问的页面或API,你也应该应用一个类似的认证中间件(可能略有不同,例如不重定向,而是返回401 Unauthorized)。
// 例如,一个用于保护通用路由的中间件
module.exports.isAuthenticated = async (req, res, next) => {
try {
const token = req.cookies ? req.cookies.node1 : null;
const decoded = token ? jwt.verify(token, "FULLSTACKWEBDEVELOPMENTMEANMERNBATCH") : null;
if (decoded) {
req.user = decoded.id; // 将用户ID附加到请求对象
return next(); // 允许访问
}
// 如果没有有效token,重定向到登录页或返回错误
res.redirect('/login');
// 或者 res.status(401).send("Unauthorized");
} catch (err) {
console.error("Protected route auth error:", err.message);
res.redirect('/login');
// 或者 res.status(401).send("Unauthorized");
}
};
// 在路由中使用
router.get("/dashboard", isAuthenticated, (req, res) => {
res.render("dashboard", { userId: req.user });
});错误处理: 在 catch 块中,除了 console.error,还可以根据具体需求进行更精细的错误处理,例如向用户显示友好的错误消息。
通过引入一个简单的 isLoggedIn 认证中间件,并结合已有的JWT和Cookie机制,我们成功地解决了Node.js应用中重复登录的问题。这个中间件能够智能地检测用户的登录状态,并根据情况进行重定向或允许页面渲染,极大地提升了用户体验。同时,实现登出功能和遵循安全最佳实践,可以确保认证系统的健壮性和安全性。