安全最佳实践 #

一、安全概述 #

1.1 常见安全威胁 #

威胁 说明
XSS 跨站脚本攻击
CSRF 跨站请求伪造
SQL注入 数据库注入攻击
点击劫持 iframe嵌入攻击
中间人攻击 数据传输劫持

1.2 Hapi安全特性 #

  • 内置输入验证
  • 安全的Cookie处理
  • CORS配置
  • 认证授权系统

二、输入验证 #

2.1 Joi验证 #

javascript
const Joi = require('@hapi/joi');

server.route({
    method: 'POST',
    path: '/users',
    options: {
        validate: {
            payload: Joi.object({
                name: Joi.string().min(2).max(50).required(),
                email: Joi.string().email().required(),
                password: Joi.string().min(8).max(50).required()
                    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
            })
        }
    },
    handler: createUser
});

2.2 输入清理 #

javascript
server.ext('onPreHandler', (request, h) => {
    if (request.payload) {
        request.payload = sanitizeInput(request.payload);
    }
    return h.continue;
});

function sanitizeInput(data) {
    if (typeof data === 'string') {
        return data.replace(/<[^>]*>/g, '');
    }
    if (typeof data === 'object') {
        Object.keys(data).forEach(key => {
            data[key] = sanitizeInput(data[key]);
        });
    }
    return data;
}

2.3 SQL注入防护 #

javascript
server.route({
    method: 'GET',
    path: '/users',
    handler: async (request, h) => {
        const { name } = request.query;
        
        const query = 'SELECT * FROM users WHERE name = $1';
        const result = await db.query(query, [name]);
        
        return result.rows;
    }
});

三、安全响应头 #

3.1 全局安全头 #

javascript
server.ext('onPreResponse', (request, h) => {
    const response = request.response;
    
    if (!response.isBoom) {
        response
            .header('X-Content-Type-Options', 'nosniff')
            .header('X-Frame-Options', 'DENY')
            .header('X-XSS-Protection', '1; mode=block')
            .header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
            .header('Content-Security-Policy', "default-src 'self'");
    }
    
    return h.continue;
});

3.2 安全头说明 #

说明
X-Content-Type-Options 防止MIME类型嗅探
X-Frame-Options 防止点击劫持
X-XSS-Protection XSS过滤器
Strict-Transport-Security 强制HTTPS
Content-Security-Policy 内容安全策略

3.3 Content-Security-Policy #

javascript
server.ext('onPreResponse', (request, h) => {
    const response = request.response;
    
    if (!response.isBoom) {
        response.header('Content-Security-Policy', [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data:",
            "font-src 'self'",
            "connect-src 'self'",
            "frame-ancestors 'none'"
        ].join('; '));
    }
    
    return h.continue;
});

四、Cookie安全 #

4.1 安全Cookie配置 #

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

4.2 Cookie选项说明 #

选项 说明
isSecure 仅HTTPS传输
isHttpOnly 禁止JavaScript访问
sameSite 同站策略
ttl 有效期

4.3 SameSite配置 #

javascript
server.state('sessionId', {
    sameSite: 'Strict'
});

server.state('analytics', {
    sameSite: 'Lax'
});

五、CSRF防护 #

5.1 安装Crumb #

bash
npm install @hapi/crumb

5.2 配置CSRF #

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

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

5.3 使用CSRF #

javascript
server.route({
    method: 'POST',
    path: '/form',
    options: {
        plugins: {
            crumb: true
        }
    },
    handler: (request, h) => {
        return { message: 'Form submitted' };
    }
});

六、认证安全 #

6.1 密码安全 #

javascript
const bcrypt = require('bcrypt');
const saltRounds = 12;

server.route({
    method: 'POST',
    path: '/register',
    handler: async (request, h) => {
        const { password } = request.payload;
        
        const hashedPassword = await bcrypt.hash(password, saltRounds);
        
        const user = await User.create({
            ...request.payload,
            password: hashedPassword
        });
        
        return h.response(user).code(201);
    }
});

6.2 登录限流 #

javascript
const loginAttempts = new Map();

server.route({
    method: 'POST',
    path: '/login',
    handler: async (request, h) => {
        const { username } = request.payload;
        const ip = request.info.remoteAddress;
        const key = `${ip}:${username}`;
        
        const attempts = loginAttempts.get(key) || 0;
        
        if (attempts >= 5) {
            throw Boom.tooManyRequests('Too many login attempts');
        }
        
        const user = await authenticate(request.payload);
        
        if (!user) {
            loginAttempts.set(key, attempts + 1);
            setTimeout(() => loginAttempts.delete(key), 15 * 60 * 1000);
            throw Boom.unauthorized('Invalid credentials');
        }
        
        loginAttempts.delete(key);
        return { token: generateToken(user) };
    }
});

6.3 JWT安全 #

javascript
server.auth.strategy('jwt', 'jwt', {
    keys: process.env.JWT_SECRET,
    verify: {
        aud: 'urn:audience:myapp',
        iss: 'urn:issuer:myapp',
        sub: false,
        nbf: true,
        exp: true,
        maxAgeSec: 3600
    },
    validate: (artifacts, request, h) => {
        return {
            isValid: true,
            credentials: artifacts.decoded.payload
        };
    }
});

七、CORS配置 #

7.1 全局CORS #

javascript
const server = Hapi.server({
    port: 3000,
    routes: {
        cors: {
            origin: ['https://example.com'],
            headers: ['Accept', 'Content-Type', 'Authorization'],
            methods: ['GET', 'POST', 'PUT', 'DELETE'],
            credentials: true
        }
    }
});

7.2 路由级CORS #

javascript
server.route({
    method: 'GET',
    path: '/api/data',
    options: {
        cors: {
            origin: ['https://trusted-domain.com'],
            headers: ['Accept', 'Content-Type'],
            additionalHeaders: ['X-Custom-Header']
        }
    },
    handler: (request, h) => {
        return { data: 'value' };
    }
});

八、HTTPS配置 #

8.1 启用HTTPS #

javascript
const fs = require('fs');
const tls = {
    key: fs.readFileSync('server.key'),
    cert: fs.readFileSync('server.crt')
};

const server = Hapi.server({
    port: 443,
    tls: tls
});

8.2 HTTP重定向 #

javascript
const http = require('http');

http.createServer((req, res) => {
    res.writeHead(301, {
        Location: `https://${req.headers.host}${req.url}`
    });
    res.end();
}).listen(80);

九、日志安全 #

9.1 敏感信息过滤 #

javascript
server.ext('onPreResponse', (request, h) => {
    const response = request.response;
    
    if (response.isBoom) {
        console.error({
            timestamp: new Date().toISOString(),
            path: request.path,
            method: request.method,
            error: response.message,
            stack: process.env.NODE_ENV === 'development' ? response.stack : undefined
        });
    }
    
    return h.continue;
});

9.2 请求日志 #

javascript
server.events.on('response', (request) => {
    const log = {
        timestamp: new Date().toISOString(),
        method: request.method,
        path: request.path,
        status: request.response.statusCode,
        ip: request.info.remoteAddress,
        userAgent: request.headers['user-agent']
    };
    
    console.log(JSON.stringify(log));
});

十、安全检查清单 #

10.1 检查项 #

  • [ ] 输入验证
  • [ ] 输出编码
  • [ ] 安全响应头
  • [ ] Cookie安全配置
  • [ ] CSRF防护
  • [ ] 认证安全
  • [ ] HTTPS启用
  • [ ] CORS配置
  • [ ] 错误处理
  • [ ] 日志安全

10.2 安全配置示例 #

javascript
const Hapi = require('@hapi/hapi');
const Crumb = require('@hapi/crumb');
const Joi = require('@hapi/joi');

const init = async () => {
    const server = Hapi.server({
        port: 3000,
        routes: {
            cors: {
                origin: ['https://example.com'],
                credentials: true
            },
            security: {
                hsts: { maxAge: 31536000, includeSubDomains: true },
                xframe: 'deny',
                xss: true,
                noSniff: true
            }
        }
    });
    
    await server.register({
        plugin: Crumb,
        options: {
            cookieOptions: {
                isSecure: true,
                isHttpOnly: true
            }
        }
    });
    
    server.state('sessionId', {
        ttl: 24 * 60 * 60 * 1000,
        isSecure: true,
        isHttpOnly: true,
        sameSite: 'Strict'
    });
    
    server.ext('onPreResponse', (request, h) => {
        const response = request.response;
        
        if (!response.isBoom) {
            response
                .header('X-Content-Type-Options', 'nosniff')
                .header('X-Frame-Options', 'DENY')
                .header('X-XSS-Protection', '1; mode=block');
        }
        
        return h.continue;
    });
    
    await server.start();
    console.log('Server running on %s', server.info.uri);
};

init();

十一、总结 #

安全最佳实践要点:

安全措施 说明
输入验证 Joi验证
安全响应头 X-Frame-Options等
Cookie安全 Secure, HttpOnly
CSRF防护 @hapi/crumb
认证安全 密码加密、限流
HTTPS TLS配置

下一步,让我们学习测试!

最后更新:2026-03-28