用户认证系统 #

一、认证概述 #

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