JWT完全ガイド2026 - 構造、セキュリティ、ベストプラクティス総まとめ
JSON Web Tokenの構造と仕組みからセキュリティの脆弱性とベストプラクティスまで。実践的なコード例でJWT認証をマスターします。
Toolypet Team
Development Team
JWT完全ガイド2026
「JWT?ただのトークンでしょ?」
その通りです。しかし、その「ただのトークン」が正しく実装されていないと、深刻なセキュリティ問題になります。2026年時点で、AI生成の認証コードの48%にセキュリティの脆弱性が存在するという研究結果もあります。
このガイドでは、JWTの構造から実践的なセキュリティのベストプラクティスまで完全に解説します。
JWTとは?
JWT(JSON Web Token)は、二者間で安全に情報を伝達するためのコンパクトで自己完結型の方式です。
主な特徴
| 特徴 | 説明 |
|---|---|
| Stateless | サーバーにセッション保存不要 |
| Self-contained | 必要な情報がトークン内に含まれる |
| Compact | URL、HTTPヘッダーで使用可能 |
| Signed | 改ざん検知可能 |
使用例
- API認証: Bearerトークンを使用したAPIリクエスト
- SSO(シングルサインオン): 複数のサービス間での認証共有
- 情報交換: 署名されたデータの伝達
- Authorization: 権限/ロール情報の包含
JWT構造
JWTは.(ドット)で区切られた3つのBase64URLエンコードされた部分で構成されます。
xxxxx.yyyyy.zzzzz
| | |
Header Payload Signature
1. Header
{
"alg": "HS256",
"typ": "JWT"
}
| フィールド | 説明 |
|---|---|
alg | 署名アルゴリズム(HS256、RS256など) |
typ | トークンタイプ(JWT) |
2. Payload (Claims)
{
"sub": "user123",
"name": "山田太郎",
"email": "yamada@example.com",
"role": "admin",
"iat": 1708502400,
"exp": 1708588800
}
登録済みクレーム(Registered Claims)
| クレーム | 説明 | 例 |
|---|---|---|
iss | 発行者(Issuer) | "https://auth.example.com" |
sub | 主体(Subject) | "user123" |
aud | 対象者(Audience) | "https://api.example.com" |
exp | 有効期限(Expiration) | 1708588800 |
nbf | 有効開始時間(Not Before) | 1708502400 |
iat | 発行時間(Issued At) | 1708502400 |
jti | JWT固有ID | "abc123" |
パブリック/プライベートクレーム
{
"role": "admin", // プライベートクレーム
"email": "user@example.com",
"permissions": ["read", "write"]
}
3. Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
署名アルゴリズム
対称鍵(Symmetric)
| アルゴリズム | 説明 | 使用例 |
|---|---|---|
| HS256 | HMAC + SHA-256 | 単一サーバー、シンプルな実装 |
| HS384 | HMAC + SHA-384 | より強力なセキュリティ |
| HS512 | HMAC + SHA-512 | 最高レベルのセキュリティ |
// Node.js - HS256
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: 123, role: 'admin' },
'your-256-bit-secret', // 同じシークレットで検証
{ algorithm: 'HS256', expiresIn: '1h' }
);
非対称鍵(Asymmetric)
| アルゴリズム | 説明 | 使用例 |
|---|---|---|
| RS256 | RSA + SHA-256 | マイクロサービス、公開検証 |
| RS384 | RSA + SHA-384 | 高いセキュリティ要件 |
| RS512 | RSA + SHA-512 | 最高レベルのセキュリティ |
| ES256 | ECDSA + P-256 | 短い鍵、モバイル |
// RS256 - 非対称
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// 発行(Private Key)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 検証(Public Key)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
アルゴリズム選択基準
| シナリオ | 推奨アルゴリズム |
|---|---|
| 単一サーバー | HS256 |
| マイクロサービス | RS256 |
| 公開検証が必要 | RS256 / ES256 |
| モバイル/IoT | ES256(短い鍵) |
実践的な実装
Node.js (Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET = process.env.JWT_SECRET; // 環境変数から読み込み
// トークン発行
app.post('/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' });
}
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
SECRET,
{
expiresIn: '1h',
issuer: 'your-app-name',
}
);
res.json({ token });
});
// ミドルウェアで検証
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'your-app-name',
algorithms: ['HS256'],
});
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// 保護されたルート
app.get('/protected', authMiddleware, (req, res) => {
res.json({ message: `Hello, ${req.user.email}` });
});
Python (FastAPI)
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta
import os
app = FastAPI()
security = HTTPBearer()
SECRET = os.environ.get("JWT_SECRET")
# トークン発行
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
}
return jwt.encode(payload, SECRET, algorithm="HS256")
# トークン検証
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# 保護されたエンドポイント
@app.get("/protected")
def protected_route(user: dict = Depends(verify_token)):
return {"message": f"Hello, {user['sub']}"}
セキュリティの脆弱性と対策
1. Algorithm None攻撃
// 悪意のあるヘッダー
{
"alg": "none",
"typ": "JWT"
}
対策:
// 許可するアルゴリズムを明示
jwt.verify(token, secret, { algorithms: ['HS256'] }); // OK
jwt.verify(token, secret); // 危険
2. シークレットキーの脆弱性
// 危険
const SECRET = 'secret123';
// 安全(最低256ビット)
const SECRET = crypto.randomBytes(32).toString('hex');
3. 機密情報の漏洩
// 危険: PayloadはBase64デコード可能
{
"password": "user_password", // 絶対禁止!
"creditCard": "1234-5678-9012-3456"
}
// 安全
{
"sub": "user123",
"role": "admin"
}
4. トークン盗難
対策戦略:
- 短い有効期限(15分〜1時間)
- Refresh Tokenの使用
- HTTPS必須
- HttpOnly Cookie(XSS防止)
5. JWTの再利用(Replay Attack)
// jti(JWT ID)を使用
const token = jwt.sign({
sub: user.id,
jti: crypto.randomUUID(), // 一意のID
}, SECRET);
// サーバーでjtiブラックリストを管理
Access Token + Refresh Token
なぜ必要か?
| トークン | 有効期間 | 用途 |
|---|---|---|
| Access Token | 15分〜1時間 | API認証 |
| Refresh Token | 7日〜30日 | Access Tokenの更新 |
実装例
// ログイン時に両方のトークンを発行
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
const accessToken = jwt.sign(
{ sub: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Refresh TokenはHttpOnly Cookieで
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
});
// トークン更新
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
JWTデバッグ
トークンのデコード
JWT Decoderにトークンを貼り付けると:
- Headerの確認
- Payloadの確認
- 有効期限の確認
- 署名の検証(シークレット入力時)
CLIでデコード
# Headerデコード
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Payloadデコード
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ" | base64 -d
ベストプラクティスチェックリスト
発行
- 強力なシークレットキー(256ビット以上)
- アルゴリズムを明示(HS256、RS256)
- 短い有効期限(15分〜1時間)
- 必須クレームを含める(sub、iat、exp、iss)
- 機密情報を含めない
検証
- アルゴリズムホワイトリスト(
algorithms: ['HS256']) - 有効期限の検証
- 発行者(iss)の検証
- 対象者(aud)の検証
保存と送信
- HTTPS必須
- HttpOnly Cookie(XSS防止)
- SameSite属性(CSRF防止)
- localStorageを避ける(XSS脆弱)
FAQ
Q1: JWT vs セッション、いつどちらを使う?
A:
- JWT: ステートレスAPI、マイクロサービス、モバイルアプリ
- セッション: 従来のWebアプリ、サーバーサイドレンダリング、即時ログアウトが必要な場合
Q2: JWTが盗まれたら?
A: Access Tokenは有効期限まで有効です。したがって:
- 短い有効期限を設定(15分)
- Refresh Token Rotation
- 疑わしい場合は全トークンを無効化
Q3: なぜlocalStorageに保存してはいけないのですか?
A: XSS攻撃に脆弱です。JavaScriptでアクセス可能なため、悪意のあるスクリプトがトークンを盗む可能性があります。HttpOnly Cookieの方が安全です。
Q4: RS256はHS256より安全ですか?
A: 「より安全」というより「用途が異なる」です。
- HS256: 同じシークレットを共有、単一サーバー
- RS256: 公開鍵で検証、複数サービス
Q5: トークンブラックリストは必要ですか?
A: 即時ログアウトが必要な場合は必要です。ただし、ステートレスの利点が減ります。短い有効期限 + Refresh Tokenが代替案です。
まとめ
JWTセキュリティの要点:
- 強力なシークレット: 256ビット以上のランダム
- アルゴリズム検証: ホワイトリスト方式
- 短い有効期間: Access 15分、Refresh 7日
- 安全な保存: HttpOnly + Secure Cookie
- 機密情報を除外: Payloadは誰でもデコード可能
関連ツール
| ツール | 用途 |
|---|---|
| JWT Decoder | JWTデコードと検証 |
| Base64 Encoder | Base64エンコード/デコード |
| Hash Generator | ハッシュ生成 |
著者について
Toolypet Team
Development Team
The Toolypet Team creates free, privacy-focused web tools for developers and designers. All tools run entirely in your browser with no data sent to servers.