JavaScript Node.js身份验证
引言
身份验证(Authentication)是几乎所有Web应用程序的核心组成部分,它确保用户是他们所声称的身份,并授权他们访问相应的资源。在本教程中,我们将学习如何在Node.js应用程序中实现稳健的身份验证系统,从基本的概念到实际应用。
备注
身份验证与授权是不同的概念:
- 身份验证(Authentication):确认用户是谁
- 授权(Authorization):确定用户可以做什么
身份验证基础知识
在深入代码之前,让我们先了解一些基本概念:
常见的身份验证方法
- 基于会话的身份验证:用户登录后服务器创建会话,并向客户端发送会话ID(通常存储在cookie中)
- 基于令牌的身份验证:服务器生成令牌(如JWT)而非维护会话状态
- OAuth:允许第三方应用获取有限的访问权限
- 多因素认证:结合多种验证方式(如密码+手机验证码)
密码安全最佳实践
在处理用户密码时,必须遵循安全最佳实践:
密码哈希
永远不要以明文形式存储密码!使用哈希算法(如bcrypt)来存储密码:
const bcrypt = require('bcrypt');
const saltRounds = 10;
// 哈希密码
async function hashPassword(plainPassword) {
const hash = await bcrypt.hash(plainPassword, saltRounds);
return hash;
}
// 验证密码
async function verifyPassword(plainPassword, hashedPassword) {
const match = await bcrypt.compare(plainPassword, hashedPassword);
return match;
}
// 使用示例
async function registerUser(email, password) {
try {
const hashedPassword = await hashPassword(password);
// 将邮箱和哈希后的密码存入数据库
console.log(`用户注册: ${email} 密码哈希: ${hashedPassword}`);
return true;
} catch (error) {
console.error('注册失败:', error);
return false;
}
}
基于会话的认证
会话认证是最传统的身份验证方式之一,使 用Express和express-session可以轻松实现:
设置会话中间件
const express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(session({
secret: 'your-secret-key', // 用于签名会话ID cookie的密钥
resave: false, // 强制将会话保存回会话存储
saveUninitialized: false, // 强制将"未初始化"的会话保存到存储中
cookie: {
secure: process.env.NODE_ENV === 'production', // 在生产环境中仅通过HTTPS发送cookie
maxAge: 1000 * 60 * 60 * 24 // 会话有效期为1天
}
}));
// 后续路由代码...
实现登录和退出逻辑
// 模拟一个用户数据库
const users = [
{ id: 1, email: '[email protected]', password: '$2b$10$...' } // 使用bcrypt哈希过的密码
];
// 登录路由
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// 查找用户
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 创建会话
req.session.userId = user.id;
res.json({ message: '登录成功' });
});
// 检查认证状态的中间件
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ message: '请先登录' });
}
next();
};
// 受保护的路由
app.get('/profile', requireAuth, (req, res) => {
// 获取用户信息
const user = users.find(u => u.id === req.session.userId);
res.json({ email: user.email });
});
// 登出路由
app.post('/logout', (req, res) => {
req.session.destroy();
res.json({ message: '已成功登出' });
});
基于JWT的认证
JSON Web Token (JWT) 是一种流行的无状态认证机制,特别适用于API和单页应用程序。
JWT的工作原理
实现JWT认证
首先安装所需的包:
npm install jsonwebtoken
然后实现JWT认证:
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// JWT密钥(在生产环境中应存储在环境变量中)
const JWT_SECRET = 'your-secret-key';
// 模拟用户数据库
const users = [
{ id: 1, email: '[email protected]', password: '$2b$10$...' } // bcrypt哈希
];
// 登录路由 - 生成JWT
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// 查找用户
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 生成JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '24h' } // 令牌有效期
);
res.json({ token });
});
// JWT验证中间件
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ message: '未提供认证令牌' });
}
const token = authHeader.split(' ')[1]; // 获取Bearer token
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: '令牌无效或已过期' });
}
req.user = user;
next();
});
};
// 受保护的路由
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json({ email: req.user.email });
});
提示
JWT令牌通常以Authorization: Bearer <token>
的形式在HTTP请求头中发送。
完整应用案例:使用MongoDB和Passport.js
现在让我们构建一个更完整的身份验证系统,使用流行的Passport.js库和MongoDB数据库。
安装所需包
npm install express mongoose passport passport-local passport-jwt bcrypt jsonwebtoken
设置MongoDB模型
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
name: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
// 保存前哈希密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// 验证密码方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
配置Passport策略
// config/passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models/User');
// JWT配置选项
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET || 'your-secret-key'
};
// 本地策略 - 用于登录
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: '用户不存在' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return done(null, false, { message: '密码不正确' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// JWT策略 - 用于验证令牌
passport.use(new JwtStrategy(jwtOptions, async (jwtPayload, done) => {
try {
const user = await User.findById(jwtPayload.userId);
if (user) {
return done(null, user);
} else {
return done(null, false);
}
} catch (error) {
return done(error, false);
}
}));
module.exports = passport;