Skip to content

ExpressJS - 身份验证

身份验证 是验证用户身份的过程。在 Web 应用程序中,这通常涉及用户名/电子邮件和密码。本教程演示了一种使用会话构建身份验证系统的现代、安全方法。

安全警告:为简化起见,此示例使用内存数组来存储用户。在实际应用程序中,你必须使用持久化数据库(如 PostgreSQL 或 MongoDB)来存储用户数据。

首先创建一个新的 Express 项目(请参阅环境设置章节),并安装处理会话和密码哈希所需的包:

# 用于会话管理
npm install express-session
# 用于安全地哈希密码
npm install bcrypt
# 用于渲染视图的模板引擎
npm install pug

良好的结构是关键。创建以下文件和目录:

.
├── src/
│ ├── views/ # Pug 模板
│ │ ├── login.pug
│ │ ├── signup.pug
│ │ └── protected.pug
│ └── index.js # 主应用程序文件
├── .env # 环境变量
├── package.json
└── ...

在你的根目录中创建一个 .env 文件来存储你的会话密钥。此文件绝不应提交到 Git。

.env
SESSION_SECRET='a-very-strong-and-long-random-secret-key'

步骤 3:主应用程序逻辑 (src/index.js)

Section titled “步骤 3:主应用程序逻辑 (src/index.js)”

此文件将包含我们所有的逻辑。在一个更大的应用程序中,你会将其拆分为路由、控制器和服务。

import express from 'express';
import session from 'express-session';
import bcrypt from 'bcrypt';
import 'dotenv/config'; // Loads .env file
const app = express();
const PORT = 3000;
// 内存中的用户存储(仅用于演示!)
const users = [];
// --- 中间件设置 ---
app.set('view engine', 'pug');
app.set('views', './src/views');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: false } // 在生产环境中,结合 HTTPS 设置为 true
}));
// --- 用于保护路由的自定义中间件 ---
const checkAuth = (req, res, next) => {
if (req.session.user) {
next(); // 用户已通过身份验证,继续
} else {
res.redirect('/login'); // 未通过身份验证,重定向到登录页
}
};
// --- 路由 ---
app.get('/', (req, res) => {
res.redirect('/login');
});
app.get('/signup', (req, res) => {
res.render('signup');
});
app.post('/signup', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).render('signup', { message: 'Username and password are required.' });
}
if (users.find(user => user.username === username)) {
return res.status(400).render('signup', { message: 'Username already exists.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10); // 哈希密码
const newUser = { username, password: hashedPassword };
users.push(newUser);
console.log('New user registered:', newUser);
res.redirect('/login');
} catch (error) {
console.error(error);
res.status(500).send('Server error');
}
});
app.get('/login', (req, res) => {
res.render('login');
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).render('login', { message: 'Invalid credentials.' });
}
try {
const isMatch = await bcrypt.compare(password, user.password);
if (isMatch) {
req.session.user = { username: user.username }; // 将用户存储到会话中
res.redirect('/protected');
} else {
res.status(401).render('login', { message: 'Invalid credentials.' });
}
} catch (error) {
console.error(error);
res.status(500).send('Server error');
}
});
app.get('/protected', checkAuth, (req, res) => {
res.render('protected', { username: req.session.user.username });
});
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('Could not log out.');
}
res.redirect('/login');
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

在你的 src/views 目录中创建以下三个 .pug 文件。

html
head
title Signup
body
h1 Create an Account
if message
p(style={color: 'red'}) #{message}
form(action="/signup" method="POST")
div
label(for="username") Username:
input(name="username" type="text" required)
div
label(for="password") Password:
input(name="password" type="password" required)
button(type="submit") Sign Up
p Already have an account? <a href="/login">Login</a>
html
head
title Login
body
h1 Login
if message
p(style={color: 'red'}) #{message}
form(action="/login" method="POST")
div
label(for="username") Username:
input(name="username" type="text" required)
div
label(for="password") Password:
input(name="password" type="password" required)
button(type="submit") Log In
p Don't have an account? <a href="/signup">Sign up</a>
html
head
title Protected Area
body
h1 Welcome, #{username}!
p This is a protected page. You can only see it if you are logged in.
form(action="/logout" method="POST")
button(type="submit") Log Out

运行 npm run dev 并导航到 http://localhost:3000。你将被重定向到登录页面。尝试直接访问 /protected;你将被送回登录页。现在,注册一个新账户,然后使用这些凭据登录以访问受保护页面。

现代替代方案:JWT (JSON Web Tokens)

Section titled “现代替代方案:JWT (JSON Web Tokens)”

虽然会话很棒,但另一种流行的身份验证方法是使用 JWT。这是一种无状态方法,意味着服务器无需存储会话数据。流程如下:

  1. 用户登录。
  2. 服务器验证凭据并向客户端返回一个签名的 JWT。
  3. 客户端存储 JWT(例如,在 HttpOnly cookie 中),并在后续请求的 Authorization 头部中发送它。
  4. 服务器在每个请求上验证 JWT 以识别用户。 此方法对于单页应用程序 (SPA) 和移动应用程序特别有用。