MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 实现用户认证与授权系统

2022-04-221.2k 阅读

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 还支持 POSTPUTDELETE 等常见的 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 - sessionpassport - 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 安装依赖

安装 mongodbmongoose(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 应用中实现用户认证与授权系统,并涵盖了安全考虑以及数据库整合的内容。这个系统可以作为更复杂应用的基础,根据实际需求进一步扩展和优化。