JWT トークン解説: モダン Web アプリケーションの認証
· 12分で読む
目次
JSON Web Token (JWT) は、モダン Web アプリケーションにおける認証のデファクトスタンダードとなっています。JWT は、2つの当事者間でクレームを表現するためのコンパクトで URL セーフな方法を提供し、共有セッションストレージなしで水平スケールするステートレス認証を可能にします。
REST API、マイクロサービスアーキテクチャ、シングルページアプリケーションのいずれを構築する場合でも、安全でスケーラブルな認証を実装するには JWT の理解が不可欠です。この包括的なガイドでは、JWT の仕組み、構造、セキュリティに関する考慮事項、本番環境対応のベストプラクティスについて説明します。
JWT とは?
JWT(「ジョット」と発音)は、JSON オブジェクトとして当事者間で情報を安全に送信するためのコンパクトで自己完結型のフォーマットを定義するオープン標準(RFC 7519)です。情報は、秘密鍵(HMAC)または公開鍵/秘密鍵ペア(RSA または ECDSA)を使用してデジタル署名され、トークンの整合性と真正性が保証されます。
サーバーがメモリやデータベースにセッション状態を保持する従来のセッションベース認証とは異なり、JWT はステートレスです。ユーザーの身元を確認するために必要なすべての情報は、トークン自体に含まれています。
JWT の主な使用例
- 認証: ユーザーがログインした後、サーバーは JWT を発行し、クライアントは後続のリクエストにそれを含めて身元を証明します
- 認可: JWT はユーザーの役割、権限、スコープを保持でき、サーバーはデータベース検索なしで詳細なアクセス制御の決定を行えます
- 情報交換: JWT の署名された性質により、送信者が主張する本人であり、送信中にデータが改ざんされていないことが保証されます
- シングルサインオン(SSO): JWT により、複数のドメインとサービス間でシームレスな認証が可能になります
- API 認証: モバイルアプリやサードパーティ統合は、サーバー側セッションを維持せずに API リクエストを認証できます
プロのヒント: 開発中にトークンを検査および検証するには、JWT デコーダーを使用してください。構造を理解し、本番環境に到達する前に一般的な問題を発見するのに役立ちます。
JWT 構造の詳細
JWT は、ドット(.)で区切られた3つの異なる部分で構成されています: ヘッダー.ペイロード.署名
完全な JWT は次のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4
パート1: ヘッダー
ヘッダーには通常、トークンの暗号化操作を説明する2つのフィールドが含まれています:
{
"alg": "HS256",
"typ": "JWT"
}
algは署名アルゴリズム(HS256、RS256、ES256 など)を指定しますtypはトークンタイプを宣言し、常に "JWT" です
この JSON オブジェクトは Base64Url エンコードされ、JWT の最初の部分を形成します。エンコーディングにより、HTTP ヘッダーでの送信に URL セーフでコンパクトになります。
パート2: ペイロード
ペイロードにはクレーム(ユーザーに関する記述と追加のメタデータ)が含まれます。クレームは3つのタイプに分類されます:
{
"sub": "1234567890",
"name": "Jane Developer",
"email": "[email protected]",
"role": "admin",
"iat": 1710590400,
"exp": 1710676800
}
登録済みクレームは JWT 仕様で事前定義され、標準情報を提供します:
iss(発行者): トークンを発行した者を識別しますsub(サブジェクト): トークンのサブジェクト(通常はユーザー ID)を識別しますaud(オーディエンス): 意図された受信者を識別しますexp(有効期限): トークンが期限切れになる Unix タイムスタンプnbf(有効開始時刻): この時刻より前はトークンが無効ですiat(発行時刻): トークンが作成された時刻jti(JWT ID): トークンの一意識別子
公開クレームは、IANA JSON Web Token Registry で定義するか、衝突耐性のある名前(URL など)を使用する必要があるカスタムクレームです。
プライベートクレームは、role、permissions、organizationId など、当事者間で合意されたカスタムクレームです。
重要: ペイロードは Base64Url エンコードされているだけで、暗号化されていません。パスワード、クレジットカード番号、社会保障番号などの機密情報を JWT ペイロードに保存しないでください。
パート3: 署名
署名は、トークンが改ざんされていないことを保証します。エンコードされたヘッダー、エンコードされたペイロード、秘密鍵を取得し、ヘッダーで指定されたアルゴリズムを適用することで作成されます:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
署名により、受信者は送信者が主張する本人であり、メッセージが途中で変更されていないことを確認できます。
署名アルゴリズムの比較
| アルゴリズム | タイプ | セキュリティレベル | 使用例 | 鍵管理 |
|---|---|---|---|---|
| HS256 | 対称(HMAC) | 良好 | 単一サービス、内部 API | 共有秘密 |
| RS256 | 非対称(RSA) | 非常に良好 | 複数サービス、公開 API | 公開鍵/秘密鍵ペア |
| ES256 | 非対称(ECDSA) | 優秀 | 高セキュリティ要件 | 公開鍵/秘密鍵ペア |
| PS256 | 非対称(RSA-PSS) | 優秀 | モダンアプリケーション | 公開鍵/秘密鍵ペア |
JWT 認証の仕組み
完全な認証フローを理解することで、JWT を正しく実装し、問題を効果的にトラブルシューティングできます。
完全な認証フロー
- ユーザーログイン: クライアントが認証エンドポイントに認証情報(ユーザー名/パスワード)を送信します
- 認証情報の検証: サーバーがデータベースに対して認証情報を検証します
- トークン生成: 認証が成功すると、サーバーはユーザー情報とクレームを含む JWT を作成します
- トークン配信: サーバーが JWT をクライアントに送り返します(通常はレスポンスボディ内)
- トークン保存: クライアントが JWT を保存します(メモリ、localStorage、または httpOnly クッキー内)
- 認証済みリクエスト: 後続のリクエストでは、クライアントが Authorization ヘッダーに JWT を含めます
- トークン検証: サーバーが JWT 署名を検証し、有効期限をチェックします
- アクセス許可: 有効な場合、サーバーはトークン内のクレームを使用してリクエストを処理します
認証リクエストの例
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"email": "[email protected]",
"password": "securePassword123"
}
認証レスポンスの例
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
認証済み API リクエストの例
GET /api/users/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT とセッションベース認証の比較
JWT とセッションベース認証のどちらを選択するかは、アプリケーションのアーキテクチャ、規模、要件によって異なります。
| 側面 | JWT | セッションベース |
|---|---|---|
| 状態 | ステートレス(サーバーはトークンを保存しない) | ステートフル(サーバーはセッションデータを保存) |
| スケーラビリティ | 優秀(共有ストレージ不要) | セッションストアが必要(Redis、データベース) |
| 失効 | 困難(ブラックリストまたは短い有効期限が必要) | 簡単(ストアからセッションを削除) |
| サイズ | 大きい(すべてのリクエストで送信) | 小さい(セッション ID のみ) |
| クロスドメイン | 簡単(CORS フレンドリー) | 複雑(クッキードメイン制限) |
| モバイルアプリ | 理想的(クッキーサポート不要) | 困難(クッキー処理) |
| セキュリティ | 慎重な実装が必要 | 確立されたパターン |
JWT を使用すべき場合
- 認証を共有する必要があるマイクロサービスの構築
- モバイルアプリケーションまたは SPA の開発
- サードパーティ統合用の公開 API の作成
- 複数のドメイン間でのシングルサインオン(SSO)の実装
- 共有セッションストレージなしでの水平スケーリング
セッションを使用すべき場合
- 従来のサーバーレンダリング Web アプリケーションの構築
- 即座のトークン失効機能が必要
- サーバー側制御が必要な機密データの取り扱い
- 帯域幅の最小化(セッションは小さなクッキーを使用)
- 複雑なセッション管理機能の実装
アプリケーションへの JWT 実装
人気のあるプログラミング言語とフレームワークでの実用的な実装例を見ていきましょう。
Node.js と Express
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET;
// ログインエンドポイント
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// 認証情報を検証(疑似コード)
const user = await verifyCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// JWT を生成
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ accessToken: token });
});
// 認証ミドルウェア
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
// 保護されたルート
app.get('/api/users/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
Python と Flask
from flask import Flask, request, jsonify
import jwt
import datetime
import os
app = Flask(__name__)
SECRET_KEY = os.getenv('JWT_SECRET')
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
# 認証情報を検証
user = verify_credentials(email, password)
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
# JWT を生成
token = jwt.encode({
'sub': user['id'],
'email': user['email'],
'role': user['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET_KEY, algorithm='HS256')
return jsonify({'accessToken': token})
def token_required(f):
def decorator(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'No token provided'}), 401
try:
token = token.split(' ')[1]
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.user = data
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 403
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 403
return f(*args, **kwargs)
decorator.__name__ = f.__name__
return decorator
@app.route('/api/users/profile')
@token_required
def profile():
return jsonify({'user': request.user})
クイックヒント: 秘密鍵には常に環境変数を使用してください。ソースコードにハードコードしたり、バージョン管理にコミットしたりしないでください。強力な秘密を作成するには、パスワードジェネレーターなどのツールを使用してください。