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 は、ドット(.)で区切られた3つの異なる部分で構成されています: ヘッダー.ペイロード.署名

完全な JWT は次のようになります:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4

パート1: ヘッダー

ヘッダーには通常、トークンの暗号化操作を説明する2つのフィールドが含まれています:

{
  "alg": "HS256",
  "typ": "JWT"
}

この JSON オブジェクトは Base64Url エンコードされ、JWT の最初の部分を形成します。エンコーディングにより、HTTP ヘッダーでの送信に URL セーフでコンパクトになります。

パート2: ペイロード

ペイロードにはクレーム(ユーザーに関する記述と追加のメタデータ)が含まれます。クレームは3つのタイプに分類されます:

{
  "sub": "1234567890",
  "name": "Jane Developer",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1710590400,
  "exp": 1710676800
}

登録済みクレームは JWT 仕様で事前定義され、標準情報を提供します:

公開クレームは、IANA JSON Web Token Registry で定義するか、衝突耐性のある名前(URL など)を使用する必要があるカスタムクレームです。

プライベートクレームは、rolepermissionsorganizationId など、当事者間で合意されたカスタムクレームです。

重要: ペイロードは Base64Url エンコードされているだけで、暗号化されていません。パスワード、クレジットカード番号、社会保障番号などの機密情報を JWT ペイロードに保存しないでください。

パート3: 署名

署名は、トークンが改ざんされていないことを保証します。エンコードされたヘッダー、エンコードされたペイロード、秘密鍵を取得し、ヘッダーで指定されたアルゴリズムを適用することで作成されます:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

署名により、受信者は送信者が主張する本人であり、メッセージが途中で変更されていないことを確認できます。

署名アルゴリズムの比較

アルゴリズム タイプ セキュリティレベル 使用例 鍵管理
HS256 対称(HMAC) 良好 単一サービス、内部 API 共有秘密
RS256 非対称(RSA) 非常に良好 複数サービス、公開 API 公開鍵/秘密鍵ペア
ES256 非対称(ECDSA) 優秀 高セキュリティ要件 公開鍵/秘密鍵ペア
PS256 非対称(RSA-PSS) 優秀 モダンアプリケーション 公開鍵/秘密鍵ペア

JWT 認証の仕組み

完全な認証フローを理解することで、JWT を正しく実装し、問題を効果的にトラブルシューティングできます。

完全な認証フロー

  1. ユーザーログイン: クライアントが認証エンドポイントに認証情報(ユーザー名/パスワード)を送信します
  2. 認証情報の検証: サーバーがデータベースに対して認証情報を検証します
  3. トークン生成: 認証が成功すると、サーバーはユーザー情報とクレームを含む JWT を作成します
  4. トークン配信: サーバーが JWT をクライアントに送り返します(通常はレスポンスボディ内)
  5. トークン保存: クライアントが JWT を保存します(メモリ、localStorage、または httpOnly クッキー内)
  6. 認証済みリクエスト: 後続のリクエストでは、クライアントが Authorization ヘッダーに JWT を含めます
  7. トークン検証: サーバーが JWT 署名を検証し、有効期限をチェックします
  8. アクセス許可: 有効な場合、サーバーはトークン内のクレームを使用してリクエストを処理します

認証リクエストの例

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 を使用すべき場合

セッションを使用すべき場合

アプリケーションへの 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})

クイックヒント: 秘密鍵には常に環境変数を使用してください。ソースコードにハードコードしたり、バージョン管理にコミットしたりしないでください。強力な秘密を作成するには、パスワードジェネレーターなどのツールを使用してください。

We use cookies for analytics. By continuing, you agree to our Privacy Policy.