🎄 科学と神々株式会社 アドベントカレンダー 2025
Hybrid License System Day 8: セキュリティ層の実装
API Gateway編 (3/5)
📖 はじめに
Day 8では、セキュリティ層の実装を学びます。CORS設定、Helmet.jsによるセキュリティヘッダー、レート制限、JWT検証ミドルウェアを実装し、安全なAPI Gatewayを構築しましょう。
🔒 CORS(Cross-Origin Resource Sharing)
CORSとは?
CORSは、異なるオリジン(ドメイン・プロトコル・ポート)からのリソースアクセスを制御するセキュリティ機構です。
┌─────────────────────────────────────────────────┐
│ Browser Security (Same-Origin Policy) │
│ │
│ https://example.com:443 → Same Origin │
│ https://api.example.com:443 → Different Origin│
│ http://example.com:443 → Different Protocol │
│ https://example.com:8080 → Different Port │
└─────────────────────────────────────────────────┘
CORS設定の実装
// api-gateway/src/server.js
const cors = require('cors');
// 基本的なCORS設定
app.use(cors({
origin: 'http://localhost:3000', // 許可するオリジン
credentials: true // Cookie送信を許可
}));
環境別CORS設定
// api-gateway/src/middleware/cors.js
const getCorsOptions = () => {
const env = process.env.NODE_ENV || 'development';
if (env === 'production') {
// 本番環境: ホワイトリスト方式
return {
origin: [
'https://app.example.com',
'https://admin.example.com'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining'],
maxAge: 86400 // 24時間キャッシュ
};
}
if (env === 'development') {
// 開発環境: すべて許可
return {
origin: true, // すべてのオリジンを許可
credentials: true
};
}
if (env === 'test') {
// テスト環境: 特定オリジンのみ
return {
origin: ['http://localhost:3000', 'http://localhost:8080'],
credentials: true
};
}
};
module.exports = getCorsOptions;
動的CORS検証
// api-gateway/src/middleware/dynamicCors.js
const dynamicCors = cors({
origin: function (origin, callback) {
// オリジンが送信されない場合(同一オリジン)は許可
if (!origin) {
return callback(null, true);
}
// ホワイトリストに含まれるか確認
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
// 許可されていないオリジン
console.warn(`Blocked CORS request from: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
});
app.use(dynamicCors);
🛡️ Helmet.js セキュリティヘッダー
Helmet.jsとは?
Helmet.jsは、HTTPセキュリティヘッダーを自動設定するミドルウェアです。
// api-gateway/src/server.js
const helmet = require('helmet');
// 基本的なHelmet設定
app.use(helmet());
設定されるセキュリティヘッダー
// 詳細なHelmet設定
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
// DNS Prefetch Control
dnsPrefetchControl: {
allow: false
},
// Frame Guard (Clickjacking防止)
frameguard: {
action: 'deny'
},
// Hide Powered-By Header
hidePoweredBy: true,
// HSTS (HTTP Strict Transport Security)
hsts: {
maxAge: 31536000, // 1年間
includeSubDomains: true,
preload: true
},
// IE No Open (IE8+のダウンロード対策)
ieNoOpen: true,
// No Sniff (MIMEタイプスニッフィング防止)
noSniff: true,
// Referrer Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
// XSS Filter
xssFilter: true
}));
レスポンスヘッダー例
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'
X-DNS-Prefetch-Control: off
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Download-Options: noopen
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-XSS-Protection: 1; mode=block
🚦 レート制限(Rate Limiting)
express-rate-limitの実装
// api-gateway/src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
// IP単位のレート制限
const ipLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分間
max: 100, // 最大100リクエスト
message: {
error: 'Too many requests',
message: 'You have exceeded the 100 requests in 15 minutes limit',
retryAfter: '15 minutes'
},
standardHeaders: true, // RateLimit-* ヘッダーを返す
legacyHeaders: false, // X-RateLimit-* ヘッダーを無効化
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
message: 'Rate limit exceeded',
retryAfter: req.rateLimit.resetTime
});
}
});
// ライセンスアクティベーション専用(より厳しい制限)
const activationLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1時間
max: 10, // 最大10リクエスト
message: 'Too many activation attempts',
skipSuccessfulRequests: false,
skipFailedRequests: false
});
module.exports = { ipLimiter, activationLimiter };
使用例
// api-gateway/src/routes/auth.js
const { ipLimiter, activationLimiter } = require('../middleware/rateLimiter');
// 全ルートにIP制限
router.use(ipLimiter);
// アクティベーションに厳しい制限
router.post('/license/activate', activationLimiter, async (req, res) => {
// アクティベーション処理
});
ユーザー単位のレート制限
// JWT検証済みユーザー単位の制限
const userLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 500, // 認証済みユーザーは多めに許可
keyGenerator: (req) => {
// JWTのuserIdをキーとして使用
return req.user?.userId || req.ip;
}
});
Redis ストアによるスケーリング
// 複数インスタンス間でレート制限を共有
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
const limiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rate_limit:'
}),
windowMs: 15 * 60 * 1000,
max: 100
});
🔑 JWT検証ミドルウェア
JWT検証の実装
// api-gateway/src/middleware/jwtVerify.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
function verifyJWT(req, res, next) {
// Authorizationヘッダーを取得
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Unauthorized',
message: 'No authorization header'
});
}
// Bearer トークンを抽出
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid authorization format. Expected: Bearer <token>'
});
}
const token = parts[1];
try {
// JWT検証
const decoded = jwt.verify(token, JWT_SECRET);
// リクエストオブジェクトにユーザー情報を追加
req.user = {
userId: decoded.userId,
email: decoded.email,
plan: decoded.plan,
clientId: decoded.clientId
};
// トークンの有効期限チェック
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp < now) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Token expired'
});
}
next();
} catch (error) {
console.error('JWT verification failed:', error.message);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Token expired',
expiredAt: error.expiredAt
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token'
});
}
return res.status(401).json({
error: 'Unauthorized',
message: 'Token verification failed'
});
}
}
module.exports = verifyJWT;
オプショナルJWT検証
// JWT検証(トークンがある場合のみ)
function optionalJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
// トークンがなくても続行
return next();
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return next();
}
const token = parts[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
} catch (error) {
// エラーでも続行(ログは記録)
console.warn('Optional JWT verification failed:', error.message);
}
next();
}
JWT検証の使用例
// api-gateway/src/routes/auth.js
const verifyJWT = require('../middleware/jwtVerify');
// 公開エンドポイント(JWT不要)
router.post('/license/activate', async (req, res) => {
// アクティベーション処理
});
// 保護されたエンドポイント(JWT必須)
router.get('/license/validate', verifyJWT, async (req, res) => {
// req.user にユーザー情報が含まれる
const { userId, email, plan, clientId } = req.user;
// 検証処理
res.json({
valid: true,
user: { userId, email, plan, clientId }
});
});
🔐 統合セキュリティミドルウェア
セキュリティスタックの統合
// api-gateway/src/middleware/security.js
const helmet = require('helmet');
const cors = require('cors');
const { ipLimiter } = require('./rateLimiter');
const getCorsOptions = require('./cors');
function applySecurity(app) {
// 1. Helmet.js セキュリティヘッダー
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// 2. CORS設定
app.use(cors(getCorsOptions()));
// 3. レート制限
app.use(ipLimiter);
// 4. リクエストサイズ制限
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 5. セキュリティログ
app.use((req, res, next) => {
console.log({
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('user-agent')
});
next();
});
}
module.exports = applySecurity;
サーバー起動時の設定
// api-gateway/src/server.js
const express = require('express');
const applySecurity = require('./middleware/security');
const app = express();
// セキュリティミドルウェアを適用
applySecurity(app);
// ルーターを設定
const authRoutes = require('./routes/auth');
const adminRoutes = require('./routes/admin');
app.use('/api/v1/license', authRoutes);
app.use('/api/v1/admin', adminRoutes);
// エラーハンドリング
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal server error'
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API Gateway running on port ${PORT}`);
});
🧪 セキュリティテスト
CORS テスト
// tests/security/cors.test.js
const request = require('supertest');
const app = require('../src/server');
describe('CORS Security', () => {
it('should allow requests from allowed origin', async () => {
const response = await request(app)
.get('/api/v1/health')
.set('Origin', 'https://app.example.com');
expect(response.headers['access-control-allow-origin']).toBe('https://app.example.com');
});
it('should block requests from disallowed origin', async () => {
const response = await request(app)
.get('/api/v1/health')
.set('Origin', 'https://malicious.com');
expect(response.status).toBe(403);
});
});
レート制限テスト
describe('Rate Limiting', () => {
it('should allow requests within limit', async () => {
for (let i = 0; i < 100; i++) {
const response = await request(app).get('/api/v1/health');
expect(response.status).toBe(200);
}
});
it('should block requests exceeding limit', async () => {
// 100リクエスト送信
for (let i = 0; i < 100; i++) {
await request(app).get('/api/v1/health');
}
// 101番目はブロックされる
const response = await request(app).get('/api/v1/health');
expect(response.status).toBe(429);
expect(response.body.error).toBe('Too many requests');
});
});
JWT検証テスト
describe('JWT Authentication', () => {
it('should reject requests without token', async () => {
const response = await request(app).get('/api/v1/license/validate');
expect(response.status).toBe(401);
});
it('should accept requests with valid token', async () => {
const token = generateTestToken({ userId: 'test-user' });
const response = await request(app)
.get('/api/v1/license/validate')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
it('should reject expired tokens', async () => {
const expiredToken = generateTestToken({ userId: 'test-user' }, { expiresIn: '-1h' });
const response = await request(app)
.get('/api/v1/license/validate')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
expect(response.body.message).toBe('Token expired');
});
});
🎯 次のステップ
Day 9では、ヘルスチェックと監視について学びます。ヘルスチェックエンドポイント、サービス監視、メトリクス収集の実装を理解しましょう。
🔗 関連リンク
次回予告: Day 9では、ヘルスチェックエンドポイントとPrometheusメトリクス収集の実装を詳しく解説します!
Copyright © 2025 Gods & Golem, Inc. All rights reserved.