Node.js Express 实现用户认证与授权系统
1. 认识用户认证与授权
在现代 Web 应用程序开发中,用户认证(Authentication)和授权(Authorization)是至关重要的环节。用户认证主要解决“你是谁”的问题,确保只有合法用户能够访问应用程序。而授权则侧重于“你能做什么”,决定已认证用户对特定资源的访问权限。
1.1 用户认证的常见方式
- 用户名和密码:这是最传统也是最广泛使用的认证方式。用户在登录时输入预先注册的用户名和密码,服务器将其与存储在数据库中的信息进行比对,若匹配则认证成功。例如,在一个简单的博客系统中,用户注册时设置用户名和密码,登录时输入相同信息以获取访问权限。
- 第三方登录:借助如 Google、Facebook 或微信等第三方平台进行认证。这种方式方便快捷,用户无需在每个应用程序中单独注册,通过第三方平台的信任关系完成认证。以一个电商应用为例,用户可以选择用微信账号登录,电商应用通过微信开放平台获取用户信息完成认证。
1.2 授权的常见模式
- 基于角色的访问控制(RBAC):将权限分配给角色,用户通过被赋予特定角色来获得相应权限。例如,在一个企业级项目管理系统中,可能有“管理员”“项目经理”“普通员工”等角色。管理员角色拥有所有权限,项目经理可以创建项目、分配任务,普通员工只能查看和更新自己的任务。
- 基于资源的访问控制(RBAC):直接将权限与资源相关联,根据用户的身份和请求的资源决定是否授权。比如在一个文件存储系统中,用户对不同的文件可能有不同的读写权限,权限直接基于文件资源进行设置。
2. Node.js 和 Express 基础
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它允许开发者使用 JavaScript 在服务器端编写应用程序。Express 则是 Node.js 中最流行的 Web 应用框架,提供了简洁的路由系统、中间件支持等功能,极大地简化了 Web 应用的开发。
2.1 安装 Node.js 和 Express
首先,需要从 Node.js 官方网站(https://nodejs.org/)下载并安装 Node.js。安装完成后,可以通过以下命令在全局安装 Express:
npm install -g express-generator
- 初始化 Express 项目:使用
express -e
命令(-e
表示使用 EJS 模板引擎,也可选择其他如 Pug 等)初始化一个新的 Express 项目。例如,创建一个名为auth - app
的项目:
express -e auth - app
cd auth - app
npm install
- 项目结构:初始化后的 Express 项目通常具有如下结构:
bin
:存放启动应用的脚本文件,如www
文件用于启动服务器。public
:存放静态资源,如 CSS、JavaScript 和图片文件。routes
:包含路由文件,定义了不同 URL 路径的处理逻辑。views
:存放模板文件,如 EJS 文件用于生成动态 HTML 页面。app.js
:应用的入口文件,在这里配置 Express 应用的各种中间件、路由等。
2.2 Express 路由基础
路由是 Express 应用程序的核心部分,它定义了应用如何响应客户端对不同 URL 的请求。例如,定义一个简单的 GET 请求路由:
const express = require('express');
const app = express();
app.get('/hello', (req, res) => {
res.send('Hello, World!');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- 请求方法:除了
GET
,Express 还支持POST
、PUT
、DELETE
等常见的 HTTP 请求方法。例如,处理POST
请求:
app.post('/submit', (req, res) => {
// 处理提交的数据
res.send('Data submitted successfully');
});
- 参数传递:可以通过 URL 参数或请求体传递数据。对于 URL 参数,可以这样获取:
app.get('/user/:id', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});
- 中间件:中间件是 Express 中非常重要的概念,它可以在请求到达路由之前或之后执行一些通用的操作,如日志记录、错误处理、身份验证等。定义一个简单的中间件:
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
app.use(logger);
这个 logger
中间件会在每次请求时记录请求的时间、方法和 URL。
3. 用户认证的实现
在 Express 应用中实现用户认证,通常会使用一些中间件和策略。这里我们将使用 express - session
中间件来管理用户会话,以及 passport - js
来处理认证策略。
3.1 安装依赖
首先,安装 express - session
和 passport - js
及其相关策略(这里以 passport - local
为例,用于用户名和密码认证):
npm install express - session passport passport - local
3.2 配置 Express - Session
在 app.js
文件中配置 express - session
:
const session = require('express - session');
app.use(session({
secret: 'your - secret - key',
resave: false,
saveUninitialized: true
}));
secret
:用于加密会话数据的密钥,务必保密。resave
:设置为false
表示只有在会话数据被修改时才保存到存储中。saveUninitialized
:设置为true
表示即使未初始化的会话也会保存。
3.3 配置 Passport
- 初始化 Passport:在
app.js
中引入并初始化 Passport:
const passport = require('passport');
app.use(passport.initialize());
app.use(passport.session());
- 配置本地策略(Local Strategy):本地策略用于用户名和密码认证。假设我们有一个简单的用户模型(这里用一个对象模拟,实际应用中应使用数据库):
const User = {
users: [
{ id: 1, username: 'admin', password: 'password123' }
]
};
const LocalStrategy = require('passport - local').Strategy;
passport.use(new LocalStrategy(
{ usernameField: 'username', passwordField: 'password' },
(username, password, done) => {
const user = User.users.find(u => u.username === username && u.password === password);
if (user) {
return done(null, user);
} else {
return done(null, false, { message: 'Invalid username or password' });
}
}
));
- 序列化和反序列化用户:Passport 需要知道如何将用户对象转换为适合存储在会话中的数据(序列化),以及如何从会话数据中还原用户对象(反序列化)。
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
const user = User.users.find(u => u.id === id);
done(null, user);
});
3.4 创建认证路由
在 routes
目录下的 users.js
文件中创建认证路由:
const express = require('express');
const router = express.Router();
const passport = require('passport');
router.get('/login', (req, res) => {
res.render('login');
});
router.post('/login',
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
})
);
router.get('/logout', (req, res) => {
req.logout();
res.redirect('/users/login');
});
module.exports = router;
/login
GET 请求:渲染登录页面。/login
POST 请求:使用passport.authenticate
中间件进行认证,认证成功则重定向到/dashboard
,失败则重定向回登录页面并显示错误信息。/logout
:用户登出,清除会话数据并重定向到登录页面。
3.5 保护路由
为了保护某些路由,只有认证用户才能访问,可以创建一个中间件:
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/users/login');
};
然后在需要保护的路由上使用这个中间件,例如在 routes/dashboard.js
中:
const express = require('express');
const router = express.Router();
const isAuthenticated = require('../middleware/isAuthenticated');
router.get('/', isAuthenticated, (req, res) => {
res.render('dashboard');
});
module.exports = router;
4. 用户授权的实现
在实现了用户认证后,接下来实现用户授权。这里我们将基于角色的访问控制(RBAC)来实现授权。
4.1 扩展用户模型
为用户模型添加角色信息。假设我们有“admin”和“user”两种角色:
const User = {
users: [
{ id: 1, username: 'admin', password: 'password123', role: 'admin' },
{ id: 2, username: 'user1', password: 'userpass', role: 'user' }
]
};
4.2 创建授权中间件
创建一个中间件来检查用户角色并决定是否授权访问:
const checkRole = (role) => {
return (req, res, next) => {
if (req.isAuthenticated() && req.user.role === role) {
return next();
}
res.status(403).send('Forbidden');
};
};
这个中间件接受一个角色参数,检查当前认证用户的角色是否与之匹配,匹配则允许访问,否则返回 403 禁止访问。
4.3 使用授权中间件
在需要授权的路由上使用 checkRole
中间件。例如,在 routes/admin.js
中:
const express = require('express');
const router = express.Router();
const isAuthenticated = require('../middleware/isAuthenticated');
const checkRole = require('../middleware/checkRole');
router.get('/', isAuthenticated, checkRole('admin'), (req, res) => {
res.render('admin - dashboard');
});
module.exports = router;
这里只有“admin”角色的用户在认证后才能访问 /admin
路由。
5. 安全考虑
在实现用户认证与授权系统时,安全是至关重要的。以下是一些需要注意的安全要点:
5.1 密码安全
- 密码哈希:永远不要在数据库中存储明文密码。使用如
bcrypt
这样的库对密码进行哈希处理。例如:
const bcrypt = require('bcrypt');
const saltRounds = 10;
const password = 'userpassword';
bcrypt.hash(password, saltRounds, (err, hash) => {
if (err) {
console.error(err);
} else {
// 将 hash 存储到数据库
console.log('Hashed password:', hash);
}
});
- 密码强度检查:在用户注册时,检查密码强度,要求密码包含一定长度、大小写字母、数字和特殊字符等。可以使用正则表达式实现简单的密码强度检查:
const password = 'UserPassword123!';
const passwordRegex = /^(?=.*[a - z])(?=.*[A - Z])(?=.*\d)(?=.*[@$!%*?&])[A - Za - z\d@$!%*?&]{8,}$/;
if (passwordRegex.test(password)) {
console.log('Password is strong');
} else {
console.log('Password does not meet requirements');
}
5.2 会话安全
- 会话密钥保密:如前面配置
express - session
时设置的secret
密钥,要确保其保密性,避免泄露导致会话数据被篡改。 - 会话过期:设置合理的会话过期时间,避免用户长时间处于登录状态增加安全风险。可以在
express - session
配置中添加cookie
选项:
app.use(session({
secret: 'your - secret - key',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 小时过期
}));
5.3 防止 CSRF 攻击
跨站请求伪造(CSRF)攻击是一种常见的 Web 安全威胁。可以使用 csurf
中间件来防范 CSRF 攻击。
- 安装依赖:
npm install csurf
- 配置中间件:在
app.js
中:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
// 在模板中传递 CSRF 令牌
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
- 在表单中使用 CSRF 令牌:在 EJS 模板的表单中添加 CSRF 令牌:
<form action="/submit" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- 其他表单字段 -->
<input type="submit" value="Submit">
</form>
6. 整合数据库
在实际应用中,通常需要将用户数据存储在数据库中,而不是像前面示例中使用简单的对象。这里以 MongoDB 为例,介绍如何整合数据库。
6.1 安装依赖
安装 mongodb
和 mongoose
(Mongoose 是 MongoDB 的对象建模工具,使操作数据库更方便):
npm install mongodb mongoose
6.2 连接 MongoDB
在 app.js
中连接 MongoDB:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/auth - app', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('Connected to MongoDB');
});
6.3 创建用户模型
使用 Mongoose 创建用户模型:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: { type: String, unique: true },
password: String,
role: { type: String, default: 'user' }
});
const User = mongoose.model('User', userSchema);
module.exports = User;
6.4 更新认证和授权逻辑
- 认证逻辑:更新
passport - local
策略中的用户查找逻辑,从数据库中查找用户:
const User = require('../models/user');
passport.use(new LocalStrategy(
{ usernameField: 'username', passwordField: 'password' },
async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (user && bcrypt.compareSync(password, user.password)) {
return done(null, user);
} else {
return done(null, false, { message: 'Invalid username or password' });
}
} catch (err) {
return done(err);
}
}
));
- 授权逻辑:更新
checkRole
中间件,确保从数据库中获取的用户角色信息正确用于授权:
const checkRole = (role) => {
return async (req, res, next) => {
try {
const user = await User.findById(req.user.id);
if (user && user.role === role) {
return next();
}
res.status(403).send('Forbidden');
} catch (err) {
res.status(500).send('Server error');
}
};
};
通过以上步骤,我们完成了在 Node.js Express 应用中实现用户认证与授权系统,并涵盖了安全考虑以及数据库整合的内容。这个系统可以作为更复杂应用的基础,根据实际需求进一步扩展和优化。