🎄 科学と神々株式会社 アドベントカレンダー 2025
License System Day 7: JWT(JSON Web Token)入門
📖 今日のテーマ
今日は、ライセンスキーとして使用する**JWT(JSON Web Token)**について学びます。
JWTは現代のWeb認証で最も広く使われている技術の一つです。その仕組みと利点を理解しましょう。
🎫 JWT とは?
定義
JWT = JSON Web Token
(ジェイソン・ウェブ・トークン / ジョット)
JSON形式のデータを安全に送受信するための標準規格(RFC 7519)
見た目
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsInBsYW4iOiJwcmVtaXVtIiwiaWF0IjoxNjk5NTY0ODAwLCJleHAiOjE3MzExMDA4MDB9.MEUCIQDz7vkKYYjPxQw3vR2S8tYLGF9QrX1K3N5WpZzJxGqA-QIgbHZm3pL8dE2fR7tQ4sV9uW0xY1zA2bC3dD4eE5fF6g
3つのパートが . で区切られている!
🏗️ JWT の構造
3つのパート
JWT = Header.Payload.Signature
1. Header(ヘッダー)
↓
2. Payload(ペイロード)
↓
3. Signature(署名)
1. Header(ヘッダー)
{
"alg": "ES256",
"typ": "JWT"
}
alg: 署名アルゴリズム
- ES256 = ECDSA P-256 + SHA256
- RS256 = RSA + SHA256
- HS256 = HMAC + SHA256
typ: トークンタイプ
- JWT(固定)
Base64 URL エンコード後:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload(ペイロード)
{
"user_id": "1234567890",
"plan": "premium",
"iat": 1699564800,
"exp": 1731100800
}
標準クレーム:
iss: 発行者(Issuer)
sub: 主体(Subject)
aud: 対象者(Audience)
exp: 有効期限(Expiration Time)
iat: 発行時刻(Issued At)
カスタムクレーム:
user_id: ユーザーID
plan: プラン種別
features: 利用可能機能
Base64 URL エンコード後:
eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsInBsYW4iOiJwcmVtaXVtIiwiaWF0IjoxNjk5NTY0ODAwLCJleHAiOjE3MzExMDA4MDB9
3. Signature(署名)
署名 = ECDSA(
Base64URL(Header) + "." + Base64URL(Payload),
秘密鍵
)
署名の目的:
- 改ざん検知
- 発行者の確認
例:
MEUCIQDz7vkKYYjPxQw3vR2S8tYLGF9QrX1K3N5WpZzJxGqA-QIgbHZm3pL8dE2fR7tQ4sV9uW0xY1zA2bC3dD4eE5fF6g
🔨 JWT の作成(サーバー側)
Node.js での実装
const jwt = require('jsonwebtoken');
const fs = require('fs');
// 秘密鍵の読み込み
const privateKey = fs.readFileSync('./keys/private_key.pem', 'utf8');
// ペイロードの定義
const payload = {
user_id: '1234567890',
email: 'user@example.com',
plan: 'premium',
features: {
echo: true,
advancedEcho: true,
bulkOperation: true
}
};
// JWT の生成
const token = jwt.sign(payload, privateKey, {
algorithm: 'ES256', // ECDSA P-256
expiresIn: '365d', // 365日間有効
issuer: 'license-server', // 発行者
subject: payload.user_id // 主体(ユーザーID)
});
console.log('JWT:', token);
🔍 JWT の検証(クライアント側)
Node.js での実装
const jwt = require('jsonwebtoken');
const fs = require('fs');
// 公開鍵の読み込み
const publicKey = fs.readFileSync('./keys/public_key.pem', 'utf8');
// JWT の検証
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['ES256'],
issuer: 'license-server'
});
console.log('✅ 検証成功!');
console.log('ユーザーID:', decoded.user_id);
console.log('プラン:', decoded.plan);
console.log('有効期限:', new Date(decoded.exp * 1000));
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.log('❌ トークンの有効期限が切れています');
} else if (error.name === 'JsonWebTokenError') {
console.log('❌ トークンが不正です');
} else {
console.log('❌ 検証エラー:', error.message);
}
}
✅ JWT の利点
1. Stateless(状態を持たない)
従来のセッション管理:
┌──────────┐ ┌──────────┐
│クライアント│ │サーバー │
└─────┬────┘ └────┬─────┘
│ │
│ 1. ログイン │
├─────────────────────►
│ │
│ 2. セッションID │
│◄─────────────────────┤
│ │
│ 【メモリ】
│ セッション情報:
│ - user_id: 123
│ - plan: premium
│
│ 3. リクエスト │
│ + セッションID │
├─────────────────────►
│ │
│ 【DB検索】
│ セッション情報を取得
│
│ 4. レスポンス │
│◄─────────────────────┤
問題:
- サーバーにセッション情報を保持
- スケールしにくい
- メモリ/DB負荷
JWT:
┌──────────┐ ┌──────────┐
│クライアント│ │サーバー │
└─────┬────┘ └────┬─────┘
│ │
│ 1. ログイン │
├─────────────────────►
│ │
│ 2. JWT │
│◄─────────────────────┤
│ (全情報を含む) │
│ │
│ 【保存不要】
│
│ 3. リクエスト │
│ + JWT │
├─────────────────────►
│ │
│ 【署名検証のみ】
│ DB アクセス不要!
│
│ 4. レスポンス │
│◄─────────────────────┤
利点:
- サーバーは状態を持たない
- 水平スケーリングが容易
- DB負荷が減る
2. 自己完結型
JWT に含まれる情報:
✅ ユーザーID
✅ プラン情報
✅ 権限
✅ 有効期限
→ DB を見なくても判断できる!
3. クロスドメイン対応
同じ JWT で複数のサービスにアクセス:
api.example.com ←─┐
│
cdn.example.com ←─┼─ 同じJWTで認証
│
admin.example.com ←┘
→ SSO(シングルサインオン)が簡単!
⚠️ JWT の注意点
1. ペイロードは暗号化されていない
❌ 危険な使い方:
{
"user_id": "123",
"password": "secret123", ← NG!
"credit_card": "1234-5678-..." ← NG!
}
✅ 正しい使い方:
{
"user_id": "123",
"plan": "premium",
"features": ["echo", "advanced"]
}
→ 機密情報は入れない!
2. トークンの無効化が難しい
問題: ユーザーがログアウトしても...
JWT は有効期限まで使える
対策:
1. 短い有効期限(例: 1時間)
2. リフレッシュトークンの併用
3. ブラックリストの管理
3. サイズが大きくなりがち
セッションID: 16-32 bytes
JWT: 200-500 bytes
→ 毎回のリクエストで送信
→ 帯域幅の消費が増える
対策:
- 必要最小限の情報のみ
- 圧縮の検討
🔄 リフレッシュトークンパターン
仕組み
アクセストークン(短命):
有効期限: 15分
用途: API アクセス
リフレッシュトークン(長命):
有効期限: 30日
用途: アクセストークンの再発行
フロー:
1. ログイン
→ アクセストークン + リフレッシュトークン
2. API アクセス(15分以内)
→ アクセストークンで認証
3. アクセストークン期限切れ
→ リフレッシュトークンで再発行
4. 新しいアクセストークン取得
→ 引き続き API アクセス
5. ログアウト
→ リフレッシュトークンを無効化
🌟 まとめ
今日学んだこと:
-
JWT の構造
- Header.Payload.Signature
- Base64 URL エンコード
-
JWT の作成と検証
- jwt.sign() で作成
- jwt.verify() で検証
-
JWT の利点
- Stateless
- 自己完結型
- クロスドメイン対応
-
注意点
- 機密情報は入れない
- 無効化が難しい
- サイズに注意
-
ベストプラクティス
- 短い有効期限
- リフレッシュトークンの併用
- HTTPS 必須
🎓 理解度チェック
- JWT の3つのパートは?
- Payload は暗号化されている?
- Stateless の利点は?
- なぜ機密情報を入れてはいけない?
💡 次回予告
Day 8: 署名検証の実装
- サーバー側での署名生成
- クライアント側での検証
- エラーハンドリング
- セキュリティベストプラクティス
お楽しみに!
前回: Day 6: ECDSA P-256署名の仕組み
次回: Day 8: 署名検証の実装
Happy Learning! 🎉