Cookie与Session #

一、Cookie基础 #

1.1 什么是Cookie? #

Cookie是存储在客户端的小型数据片段,用于在HTTP请求之间保持状态。

1.2 读取Cookie #

javascript
server.route({
    method: 'GET',
    path: '/cookies',
    handler: (request, h) => {
        return {
            sessionId: request.state.sessionId,
            preferences: request.state.preferences,
            allCookies: request.state
        };
    }
});

1.3 设置Cookie #

javascript
server.route({
    method: 'POST',
    path: '/set-cookie',
    handler: (request, h) => {
        return h.response({ message: 'Cookie set' })
            .state('sessionId', 'abc123');
    }
});

二、Cookie配置 #

2.1 Cookie选项 #

javascript
server.route({
    method: 'POST',
    path: '/login',
    handler: (request, h) => {
        return h.response({ message: 'Logged in' })
            .state('sessionId', 'abc123', {
                ttl: 24 * 60 * 60 * 1000,
                isSecure: true,
                isHttpOnly: true,
                path: '/',
                domain: '.example.com',
                sameSite: 'Strict'
            });
    }
});

2.2 Cookie选项说明 #

选项 说明
ttl 有效期(毫秒)
isSecure 仅HTTPS传输
isHttpOnly 禁止JavaScript访问
path Cookie路径
domain Cookie域名
sameSite 同站策略
encoding 编码方式

2.3 全局Cookie配置 #

javascript
const server = Hapi.server({
    port: 3000,
    state: {
        cookies: {
            strictHeader: false,
            ignoreErrors: true
        }
    }
});

server.state('sessionId', {
    ttl: 24 * 60 * 60 * 1000,
    isSecure: true,
    isHttpOnly: true,
    path: '/',
    encoding: 'base64json'
});

三、Cookie操作 #

3.1 设置多个Cookie #

javascript
server.route({
    method: 'GET',
    path: '/multi-cookies',
    handler: (request, h) => {
        return h.response({ message: 'Cookies set' })
            .state('user', 'john')
            .state('theme', 'dark')
            .state('language', 'zh-CN');
    }
});

3.2 清除Cookie #

javascript
server.route({
    method: 'POST',
    path: '/logout',
    handler: (request, h) => {
        return h.response({ message: 'Logged out' })
            .unstate('sessionId')
            .unstate('user');
    }
});

3.3 Cookie编码 #

javascript
server.state('data', {
    encoding: 'base64json'
});

server.route({
    method: 'GET',
    path: '/encoded',
    handler: (request, h) => {
        const data = { userId: 123, role: 'admin' };
        return h.response({ message: 'OK' })
            .state('data', data);
    }
});

四、Cookie认证 #

4.1 安装cookie插件 #

bash
npm install @hapi/cookie

4.2 配置Cookie认证 #

javascript
const Cookie = require('@hapi/cookie');

await server.register(Cookie);

server.auth.strategy('session', 'cookie', {
    cookie: {
        name: 'session',
        password: 'password-must-be-at-least-32-characters',
        isSecure: false,
        ttl: 24 * 60 * 60 * 1000
    },
    redirectTo: '/login',
    validateFunc: async (request, session) => {
        const user = await User.findById(session.id);
        
        if (!user) {
            return { valid: false };
        }
        
        return {
            valid: true,
            credentials: user
        };
    }
});

server.auth.default('session');

4.3 登录路由 #

javascript
server.route({
    method: 'POST',
    path: '/login',
    options: {
        auth: false
    },
    handler: async (request, h) => {
        const { username, password } = request.payload;
        
        const user = await User.authenticate(username, password);
        
        if (!user) {
            throw Boom.unauthorized('Invalid credentials');
        }
        
        request.cookieAuth.set({ id: user.id });
        
        return { message: 'Logged in', user };
    }
});

4.4 登出路由 #

javascript
server.route({
    method: 'POST',
    path: '/logout',
    handler: (request, h) => {
        request.cookieAuth.clear();
        return { message: 'Logged out' };
    }
});

五、Session管理 #

5.1 服务端Session #

javascript
const sessions = new Map();

server.state('sessionId', {
    ttl: 24 * 60 * 60 * 1000,
    isSecure: true,
    isHttpOnly: true
});

server.route({
    method: 'POST',
    path: '/login',
    handler: async (request, h) => {
        const { username, password } = request.payload;
        const user = await authenticate(username, password);
        
        if (!user) {
            throw Boom.unauthorized('Invalid credentials');
        }
        
        const sessionId = generateSessionId();
        sessions.set(sessionId, {
            userId: user.id,
            createdAt: Date.now()
        });
        
        return h.response({ message: 'Logged in' })
            .state('sessionId', sessionId);
    }
});

server.route({
    method: 'GET',
    path: '/profile',
    handler: (request, h) => {
        const sessionId = request.state.sessionId;
        const session = sessions.get(sessionId);
        
        if (!session) {
            throw Boom.unauthorized('Session expired');
        }
        
        return { userId: session.userId };
    }
});

5.2 Redis Session #

javascript
const Redis = require('ioredis');
const redis = new Redis();

server.route({
    method: 'POST',
    path: '/login',
    handler: async (request, h) => {
        const { username, password } = request.payload;
        const user = await authenticate(username, password);
        
        if (!user) {
            throw Boom.unauthorized('Invalid credentials');
        }
        
        const sessionId = generateSessionId();
        await redis.setex(
            `session:${sessionId}`,
            86400,
            JSON.stringify({ userId: user.id })
        );
        
        return h.response({ message: 'Logged in' })
            .state('sessionId', sessionId);
    }
});

server.route({
    method: 'GET',
    path: '/profile',
    handler: async (request, h) => {
        const sessionId = request.state.sessionId;
        const data = await redis.get(`session:${sessionId}`);
        
        if (!data) {
            throw Boom.unauthorized('Session expired');
        }
        
        const session = JSON.parse(data);
        return { userId: session.userId };
    }
});

六、JWT认证 #

6.1 安装JWT插件 #

bash
npm install @hapi/jwt

6.2 配置JWT认证 #

javascript
const Jwt = require('@hapi/jwt');

await server.register(Jwt);

server.auth.strategy('jwt', 'jwt', {
    keys: 'your-secret-key-at-least-32-characters',
    verify: {
        aud: 'urn:audience:myapp',
        iss: 'urn:issuer:myapp',
        sub: false,
        nbf: true,
        exp: true,
        maxAgeSec: 86400
    },
    validate: (artifacts, request, h) => {
        return {
            isValid: true,
            credentials: artifacts.decoded.payload
        };
    }
});

6.3 登录获取Token #

javascript
const Jwt = require('@hapi/jwt');

server.route({
    method: 'POST',
    path: '/login',
    options: {
        auth: false
    },
    handler: async (request, h) => {
        const { username, password } = request.payload;
        
        const user = await authenticate(username, password);
        
        if (!user) {
            throw Boom.unauthorized('Invalid credentials');
        }
        
        const token = Jwt.token.generate(
            {
                aud: 'urn:audience:myapp',
                iss: 'urn:issuer:myapp',
                id: user.id,
                username: user.username,
                role: user.role
            },
            {
                key: 'your-secret-key',
                algorithm: 'HS256'
            },
            {
                ttlSec: 86400
            }
        );
        
        return { token, user };
    }
});

6.4 使用Token访问 #

javascript
server.route({
    method: 'GET',
    path: '/profile',
    options: {
        auth: 'jwt'
    },
    handler: (request, h) => {
        return {
            user: request.auth.credentials
        };
    }
});

七、OAuth认证 #

7.1 安装Bell插件 #

bash
npm install @hapi/bell

7.2 配置OAuth #

javascript
const Bell = require('@hapi/bell');

await server.register(Bell);

server.auth.strategy('github', 'bell', {
    provider: 'github',
    password: 'cookie-encryption-password',
    clientId: 'your-github-client-id',
    clientSecret: 'your-github-client-secret',
    isSecure: false
});

server.route({
    method: ['GET', 'POST'],
    path: '/login/github',
    options: {
        auth: 'github',
        handler: (request, h) => {
            if (!request.auth.isAuthenticated) {
                return 'Authentication failed';
            }
            
            const profile = request.auth.credentials.profile;
            
            return h.response({
                message: 'Logged in',
                user: {
                    id: profile.id,
                    username: profile.username,
                    email: profile.email
                }
            });
        }
    }
});

八、权限控制 #

8.1 角色检查 #

javascript
const checkRole = (role) => {
    return (request, h) => {
        if (request.auth.credentials.role !== role) {
            throw Boom.forbidden('Insufficient permissions');
        }
        return h.continue;
    };
};

server.route({
    method: 'GET',
    path: '/admin',
    options: {
        auth: 'jwt',
        pre: [
            { method: checkRole('admin') }
        ]
    },
    handler: (request, h) => {
        return { message: 'Admin area' };
    }
});

8.2 权限中间件 #

javascript
server.ext('onPostAuth', (request, h) => {
    const route = request.route;
    
    if (route.settings.auth && route.settings.plugins.rbac) {
        const requiredRole = route.settings.plugins.rbac.role;
        const userRole = request.auth.credentials.role;
        
        if (userRole !== requiredRole) {
            throw Boom.forbidden('Insufficient permissions');
        }
    }
    
    return h.continue;
});

server.route({
    method: 'DELETE',
    path: '/users/{id}',
    options: {
        auth: 'jwt',
        plugins: {
            rbac: {
                role: 'admin'
            }
        }
    },
    handler: deleteUser
});

九、安全最佳实践 #

9.1 Cookie安全 #

javascript
server.state('sessionId', {
    ttl: 24 * 60 * 60 * 1000,
    isSecure: true,
    isHttpOnly: true,
    path: '/',
    sameSite: 'Strict'
});

9.2 Token安全 #

javascript
server.route({
    method: 'POST',
    path: '/login',
    handler: async (request, h) => {
        const token = generateToken(user);
        
        return h.response({ token })
            .header('X-Content-Type-Options', 'nosniff')
            .header('X-Frame-Options', 'DENY')
            .header('X-XSS-Protection', '1; mode=block');
    }
});

9.3 CSRF保护 #

bash
npm install @hapi/crumb
javascript
const Crumb = require('@hapi/crumb');

await server.register({
    plugin: Crumb,
    options: {
        cookieOptions: {
            isSecure: true
        }
    }
});

十、完整示例 #

10.1 完整认证系统 #

javascript
const Hapi = require('@hapi/hapi');
const Cookie = require('@hapi/cookie');
const Jwt = require('@hapi/jwt');
const Boom = require('@hapi/boom');

const init = async () => {
    const server = Hapi.server({ port: 3000 });
    
    await server.register([Cookie, Jwt]);
    
    server.auth.strategy('session', 'cookie', {
        cookie: {
            name: 'session',
            password: 'password-must-be-at-least-32-characters',
            isSecure: false
        },
        validateFunc: async (request, session) => {
            const user = await User.findById(session.id);
            return { valid: !!user, credentials: user };
        }
    });
    
    server.auth.strategy('jwt', 'jwt', {
        keys: 'your-secret-key',
        verify: { aud: false, iss: false },
        validate: (artifacts) => ({
            isValid: true,
            credentials: artifacts.decoded.payload
        })
    });
    
    server.route([
        {
            method: 'POST',
            path: '/login',
            options: { auth: false },
            handler: async (request, h) => {
                const { username, password } = request.payload;
                const user = await authenticate(username, password);
                
                if (!user) {
                    throw Boom.unauthorized('Invalid credentials');
                }
                
                request.cookieAuth.set({ id: user.id });
                return { message: 'Logged in', user };
            }
        },
        {
            method: 'POST',
            path: '/logout',
            handler: (request, h) => {
                request.cookieAuth.clear();
                return { message: 'Logged out' };
            }
        },
        {
            method: 'GET',
            path: '/profile',
            options: { auth: 'session' },
            handler: (request, h) => {
                return request.auth.credentials;
            }
        }
    ]);
    
    await server.start();
};

init();

十一、总结 #

Cookie与Session要点:

功能 方法
读取Cookie request.state
设置Cookie h.response().state()
清除Cookie h.response().unstate()
Cookie认证 @hapi/cookie
JWT认证 @hapi/jwt
OAuth认证 @hapi/bell

下一步,让我们学习Hapi的高级特性!

最后更新:2026-03-28