用户认证系统 #
一、认证概述 #
1.1 认证方式 #
| 方式 | 说明 |
|---|---|
| Cookie Session | 传统会话认证 |
| JWT | 无状态令牌认证 |
| OAuth | 第三方认证 |
1.2 项目依赖 #
bash
npm install @hapi/hapi @hapi/joi @hapi/boom @hapi/jwt
npm install bcrypt jsonwebtoken
二、用户模型 #
2.1 src/models/user.js #
javascript
const bcrypt = require('bcrypt');
const users = [];
let nextId = 1;
module.exports = {
findAll: () => users,
findById: (id) => users.find(u => u.id === parseInt(id)),
findByEmail: (email) => users.find(u => u.email === email),
create: async (data) => {
const hashedPassword = await bcrypt.hash(data.password, 10);
const user = {
id: nextId++,
name: data.name,
email: data.email,
password: hashedPassword,
role: data.role || 'user',
createdAt: new Date().toISOString()
};
users.push(user);
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
},
validatePassword: async (user, password) => {
return await bcrypt.compare(password, user.password);
},
update: (id, data) => {
const index = users.findIndex(u => u.id === parseInt(id));
if (index === -1) return null;
users[index] = { ...users[index], ...data };
const { password, ...userWithoutPassword } = users[index];
return userWithoutPassword;
},
delete: (id) => {
const index = users.findIndex(u => u.id === parseInt(id));
if (index === -1) return false;
users.splice(index, 1);
return true;
}
};
三、JWT配置 #
3.1 src/config/auth.js #
javascript
module.exports = {
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters',
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
issuer: 'my-app',
audience: 'my-app-users'
}
};
3.2 src/utils/jwt.js #
javascript
const jwt = require('jsonwebtoken');
const config = require('../config/auth');
const generateToken = (user) => {
return jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
},
config.jwt.secret,
{
expiresIn: config.jwt.expiresIn,
issuer: config.jwt.issuer,
audience: config.jwt.audience
}
);
};
const verifyToken = (token) => {
try {
return jwt.verify(token, config.jwt.secret, {
issuer: config.jwt.issuer,
audience: config.jwt.audience
});
} catch (error) {
return null;
}
};
module.exports = {
generateToken,
verifyToken
};
四、认证插件 #
4.1 src/plugins/auth.js #
javascript
const Jwt = require('@hapi/jwt');
const config = require('../config/auth');
const User = require('../models/user');
const authPlugin = {
name: 'auth',
register: async (server, options) => {
await server.register(Jwt);
server.auth.strategy('jwt', 'jwt', {
keys: config.jwt.secret,
verify: {
aud: config.jwt.audience,
iss: config.jwt.issuer,
sub: false,
nbf: true,
exp: true,
maxAgeSec: 7 * 24 * 60 * 60
},
validate: async (artifacts, request, h) => {
const user = User.findById(artifacts.decoded.payload.id);
if (!user) {
return { isValid: false };
}
return {
isValid: true,
credentials: {
id: user.id,
email: user.email,
role: user.role
}
};
}
});
server.auth.default('jwt');
}
};
module.exports = authPlugin;
五、认证处理函数 #
5.1 src/handlers/auth.js #
javascript
const Boom = require('@hapi/boom');
const User = require('../models/user');
const { generateToken } = require('../utils/jwt');
const register = async (request, h) => {
const { email } = request.payload;
const existingUser = User.findByEmail(email);
if (existingUser) {
throw Boom.conflict('Email already registered');
}
const user = await User.create(request.payload);
const token = generateToken(user);
return h.response({
message: 'User registered successfully',
user,
token
}).code(201);
};
const login = async (request, h) => {
const { email, password } = request.payload;
const user = User.findByEmail(email);
if (!user) {
throw Boom.unauthorized('Invalid credentials');
}
const isValid = await User.validatePassword(user, password);
if (!isValid) {
throw Boom.unauthorized('Invalid credentials');
}
const { password: _, ...userWithoutPassword } = user;
const token = generateToken(userWithoutPassword);
return {
message: 'Login successful',
user: userWithoutPassword,
token
};
};
const getProfile = async (request, h) => {
return {
user: request.auth.credentials
};
};
const updateProfile = async (request, h) => {
const userId = request.auth.credentials.id;
const user = User.update(userId, request.payload);
return {
message: 'Profile updated',
user
};
};
const changePassword = async (request, h) => {
const { currentPassword, newPassword } = request.payload;
const userId = request.auth.credentials.id;
const user = User.findById(userId);
const isValid = await User.validatePassword(user, currentPassword);
if (!isValid) {
throw Boom.unauthorized('Current password is incorrect');
}
const hashedPassword = await bcrypt.hash(newPassword, 10);
User.update(userId, { password: hashedPassword });
return { message: 'Password changed successfully' };
};
module.exports = {
register,
login,
getProfile,
updateProfile,
changePassword
};
六、认证路由 #
6.1 src/routes/auth.js #
javascript
const Joi = require('joi');
const handlers = require('../handlers/auth');
module.exports = [
{
method: 'POST',
path: '/auth/register',
options: {
auth: false,
description: 'Register new user',
tags: ['api'],
validate: {
payload: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
role: Joi.string().valid('user', 'admin')
})
}
},
handler: handlers.register
},
{
method: 'POST',
path: '/auth/login',
options: {
auth: false,
description: 'User login',
tags: ['api'],
validate: {
payload: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
})
}
},
handler: handlers.login
},
{
method: 'GET',
path: '/auth/profile',
options: {
description: 'Get user profile',
tags: ['api']
},
handler: handlers.getProfile
},
{
method: 'PUT',
path: '/auth/profile',
options: {
description: 'Update profile',
tags: ['api'],
validate: {
payload: Joi.object({
name: Joi.string().min(2).max(50)
})
}
},
handler: handlers.updateProfile
},
{
method: 'POST',
path: '/auth/change-password',
options: {
description: 'Change password',
tags: ['api'],
validate: {
payload: Joi.object({
currentPassword: Joi.string().required(),
newPassword: Joi.string().min(6).required()
})
}
},
handler: handlers.changePassword
}
];
七、权限控制 #
7.1 角色检查中间件 #
javascript
const Boom = require('@hapi/boom');
const checkRole = (...roles) => {
return (request, h) => {
const userRole = request.auth.credentials.role;
if (!roles.includes(userRole)) {
throw Boom.forbidden('Insufficient permissions');
}
return h.continue;
};
};
module.exports = checkRole;
7.2 使用权限检查 #
javascript
const checkRole = require('../utils/checkRole');
server.route({
method: 'DELETE',
path: '/users/{id}',
options: {
pre: [
{ method: checkRole('admin') }
]
},
handler: deleteUser
});
八、完整入口文件 #
8.1 src/index.js #
javascript
require('dotenv').config();
const Hapi = require('@hapi/hapi');
const config = require('./config');
const authPlugin = require('./plugins/auth');
const errorHandler = require('./plugins/errorHandler');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const init = async () => {
const server = Hapi.server({
port: config.port,
host: config.host,
routes: {
cors: true
}
});
await server.register(authPlugin);
await server.register(errorHandler);
server.route([
{
method: 'GET',
path: '/',
options: { auth: false },
handler: (request, h) => {
return { message: 'Welcome to Auth API' };
}
},
{
method: 'GET',
path: '/health',
options: { auth: false },
handler: (request, h) => {
return { status: 'ok' };
}
},
...authRoutes,
...userRoutes
]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
init();
九、API测试 #
9.1 注册用户 #
bash
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","password":"password123"}'
9.2 登录 #
bash
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"password123"}'
9.3 获取个人资料 #
bash
curl http://localhost:3000/auth/profile \
-H "Authorization: Bearer YOUR_TOKEN"
十、总结 #
认证系统要点:
| 功能 | 路径 | 方法 |
|---|---|---|
| 注册 | /auth/register | POST |
| 登录 | /auth/login | POST |
| 个人资料 | /auth/profile | GET |
| 更新资料 | /auth/profile | PUT |
| 修改密码 | /auth/change-password | POST |
下一步,让我们学习文件上传服务!
最后更新:2026-03-28