メインコンテンツにスキップ

🎫 JWT トークン完全理解

📖 定義

JWT(JSON Web Token)は、クライアントとサーバー間で情報を安全に転送するためのJSONベースのトークンです。3つの部分(Header、Payload、Signature)で構成され、サーバーで発行しクライアントがリクエスト時に一緒に送信して認証を処理します。セッションと異なりサーバーに状態を保存しない(stateless)ため、スケーラビリティに優れています。

🎯 比喩で理解する

遊園地の入場券

JWTを遊園地の入場券に例えると:

通常入場券(セッション方式)
├─ 入場時にリストバンド受け取り
├─ 乗る度にスタッフがリストバンド確認
└─ スタッフが全訪問者を記憶する必要(サーバー負担)

VIP入場券(JWT方式)
├─ 入場時に偽造不可能なスタンプ付きチケット
├─ チケットに名前、有効期限、等級記録
├─ 乗る度にチケットを見せるだけ
└─ スタッフはスタンプのみ確認(サーバー負担少)

JWT = 偽造防止スタンプ付き情報カード

⚙️ 動作原理

1. JWT構造

JWT = Header.Payload.Signature

例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW0ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
Header (ヘッダー) Payload (ペイロード) Signature (署名)

Header (ヘッダー)

{
"alg": "HS256", // 署名アルゴリズム
"typ": "JWT" // トークンタイプ
}
// Base64でエンコード

Payload (ペイロード) - 実際のデータ

{
"userId": 123,
"username": "kim",
"email": "kim@example.com",
"role": "admin",
"iat": 1640000000, // Issued At (発行時刻)
"exp": 1640086400 // Expiration (有効期限)
}
// Base64でエンコード

Signature (署名) - 偽造防止

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // 秘密鍵(サーバーのみ知っている)
)

2. JWT認証フロー

1. ログイン
クライアント → サーバー: username, password
サーバー: 認証成功 → JWT生成して返却

2. リクエスト時にJWT含める
クライアント → サーバー: Authorization: Bearer <JWT>

3. サーバーでJWT検証
- 署名確認(偽造有無)
- 有効期限確認
- Payloadからユーザー情報抽出

4. レスポンス
サーバー → クライアント: 要求されたデータ

💡 実際の例

JWT生成と検証 (Node.js)

const jwt = require('jsonwebtoken');

// 秘密鍵(環境変数で管理推奨)
const SECRET_KEY = 'your-secret-key-keep-it-safe';

// ========== JWT生成 ==========
function generateToken(user) {
// Payload定義
const payload = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role
};

// オプション
const options = {
expiresIn: '1h', // 1時間後に期限切れ
// expiresIn: '7d', // 7日
// expiresIn: '30m', // 30分
issuer: 'my-app', // 発行者
subject: 'user-auth' // 用途
};

// JWT生成
const token = jwt.sign(payload, SECRET_KEY, options);
return token;
}

// ========== JWT検証 ==========
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
console.log('検証成功:', decoded);
/*
{
userId: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin',
iat: 1640000000,
exp: 1640003600
}
*/
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('トークン期限切れ');
} else if (error.name === 'JsonWebTokenError') {
console.error('無効なトークン');
}
return null;
}
}

// ========== 使用例 ==========
const user = {
id: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin'
};

const token = generateToken(user);
console.log('生成されたJWT:', token);

const decoded = verifyToken(token);
console.log('デコード:', decoded);

ログインAPI実装

const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const users = []; // 実際にはデータベース使用

// ========== 会員登録 ==========
app.post('/api/register', async (req, res) => {
const { username, password, email } = req.body;

// パスワードハッシュ化
const hashedPassword = await bcrypt.hash(password, 10);

const user = {
id: users.length + 1,
username,
email,
password: hashedPassword
};

users.push(user);

res.json({ message: '会員登録成功', userId: user.id });
});

// ========== ログイン ==========
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;

// ユーザー検索
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'ユーザーが見つかりません' });
}

// パスワード確認
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'パスワードが間違っています' });
}

// JWT生成
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email
},
SECRET_KEY,
{ expiresIn: '1h' }
);

// Refresh Tokenも一緒に発行(オプション)
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '7d' }
);

res.json({
message: 'ログイン成功',
accessToken: token,
refreshToken: refreshToken
});
});

// ========== 認証ミドルウェア ==========
function authenticateToken(req, res, next) {
// Authorizationヘッダーからトークン抽出
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

if (!token) {
return res.status(401).json({ error: 'トークンがありません' });
}

jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'トークンが期限切れです' });
}
return res.status(403).json({ error: '無効なトークンです' });
}

// 検証成功 - ユーザー情報をreqに保存
req.user = user;
next();
});
}

// ========== 保護されたルート ==========
app.get('/api/profile', authenticateToken, (req, res) => {
// req.userはJWTから抽出した情報
res.json({
message: 'プロフィール情報',
user: req.user
});
});

app.get('/api/admin', authenticateToken, (req, res) => {
// 権限確認
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '管理者権限が必要です' });
}

res.json({ message: '管理者ページ' });
});

app.listen(3000, () => {
console.log('サーバー実行中: http://localhost:3000');
});

フロントエンドでJWT使用

// ========== ログイン ==========
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});

const data = await response.json();

if (response.ok) {
// JWTをローカルストレージに保存
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);

console.log('ログイン成功!');
}
}

// ========== APIリクエスト時にJWT含める ==========
async function fetchProfile() {
const token = localStorage.getItem('accessToken');

const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});

if (response.status === 401) {
// トークン期限切れ → ログインページへ
window.location.href = '/login';
return;
}

const data = await response.json();
console.log('プロフィール:', data);
}

// ========== Axiosインターセプターで自動化 ==========
import axios from 'axios';

// リクエストインターセプター: 全リクエストにJWT自動追加
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);

// レスポンスインターセプター: 401エラー時に自動ログアウト
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);

// これで全リクエストに自動的にJWT含まれる!
axios.get('/api/profile').then(response => {
console.log(response.data);
});

Refresh Token実装

// ========== Access Token + Refresh Tokenパターン ==========

// Access Token: 短い寿命(15分)
// Refresh Token: 長い寿命(7日)

app.post('/api/login', async (req, res) => {
// ... ログイン検証 ...

const accessToken = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' } // 15分
);

const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET_KEY,
{ expiresIn: '7d' } // 7日
);

// Refresh TokenはDBに保存(オプション)
// await saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken });
});

// ========== Access Token更新 ==========
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Tokenがありません' });
}

jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: '無効なRefresh Token' });
}

// 新しいAccess Token発行
const accessToken = jwt.sign(
{ userId: user.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);

res.json({ accessToken });
});
});

// ========== フロントエンド: 自動トークン更新 ==========
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
// Refresh Tokenで新しいAccess Token取得
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });

const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);

// 元のリクエストを再試行
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Tokenも期限切れ → ログアウト
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

🤔 よくある質問

Q1. JWTとセッションの違いは?

A:

// ========== セッション方式 ==========
// サーバーにユーザー情報保存

ログイン → サーバーにセッション保存
{
sessionId: 'abc123',
userId: 123,
username: 'kim'
}

クライアントはsessionIdのみ保存
Cookie: sessionId=abc123

リクエスト時にsessionIdでサーバーのセッション照会

利点: サーバーで即座に無効化可能
欠点: サーバーメモリ/ストレージ必要、スケーラビリティ低い

// ========== JWT方式 ==========
// クライアントがトークン保存

ログイン → JWT生成して返却
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

クライアントがJWT全体を保存
localStorage.setItem('token', jwt)

リクエスト時にJWT含める、サーバーは署名のみ検証

利点: サーバー負担なし、スケーラビリティ良好
欠点: トークン無効化困難、サイズ大きい

Q2. JWTは安全ですか?

A: 正しく使用すれば安全です:

// ✅ 安全な使用法
1. HTTPS使用必須
- HTTPJWT傍受可能

2. 秘密鍵のセキュリティ
- 環境変数で管理
- 絶対にコードにハードコード禁止
const SECRET = process.env.JWT_SECRET;

3. 短い有効期限
- Access Token: 15~1時間
- Refresh Tokenで更新

4. 機密情報保存禁止
{ password: '1234', ssn: '123-45-6789' }
{ userId: 123, role: 'user' }

5. XSS防止
- LocalStorageよりHttpOnly Cookie推奨
res.cookie('token', jwt, {
httpOnly: true, // JavaScript アクセス不可
secure: true, // HTTPS only
sameSite: 'strict'
});

// ❌ 危険な使用法
1. 有効期限なし
2. HTTPで送信
3. LocalStorageに保存(XSS脆弱)
4. パスワードなどの機密情報含む

Q3. JWTを無効化するには?

A: いくつかの方法があります:

// 1. Blacklist (ブラックリスト)
// - ログアウト時にトークンをDB/Redisに保存
// - リクエスト時にブラックリスト確認

const blacklist = new Set();

app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: 'ログアウト成功' });
});

function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];

if (blacklist.has(token)) {
return res.status(401).json({ error: '無効化されたトークン' });
}

// ... JWT検証 ...
}

// 2. Redisに保存(有効期限設定)
const redis = require('redis');
const client = redis.createClient();

app.post('/api/logout', authenticateToken, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);

// トークン有効期限までブラックリストに維持
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, ttl, 'true');

res.json({ message: 'ログアウト成功' });
});

// 3. 短い寿命 + Refresh Token
// - Access Token: 15分(無効化不要)
// - Refresh TokenのみDBに保存及び無効化

// 4. バージョン管理
// - ユーザー別トークンバージョンをDBに保存
// - ログアウト時にバージョン増加

const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // DBから取得
};

// ログアウト
await db.users.update(
{ id: userId },
{ $inc: { tokenVersion: 1 } } // バージョン増加
);

Q4. JWTはどこに保存すべきですか?

A:

// オプション1: LocalStorage(簡単だがXSS脆弱)
localStorage.setItem('token', jwt);
// ❌ XSS攻撃でトークン窃取可能

// オプション2: SessionStorage(タブを閉じると削除)
sessionStorage.setItem('token', jwt);
// ❌ XSS脆弱

// オプション3: HttpOnly Cookie(最も安全、推奨)
// サーバーで設定
res.cookie('token', jwt, {
httpOnly: true, // JavaScript アクセス不可(XSS防止)
secure: true, // HTTPS only
sameSite: 'strict',// CSRF防止
maxAge: 3600000 // 1時間
});

// クライアントでは自動送信される
fetch('/api/profile', {
credentials: 'include' // Cookie含む
});

// オプション4: メモリ(最も安全だがリフレッシュ時にログアウト)
let token = null;

function login() {
// ... ログイン ...
token = response.accessToken; // 変数にのみ保存
}

// 長所短所比較
LocalStorage: 簡単だがXSS脆弱
Cookie: 安全だが設定複雑
Memory: 最も安全だがUX悪い

Q5. Access Token vs Refresh Token?

A:

// Access Token
// - 短い寿命(15分~1時間)
// - APIリクエスト毎に送信
// - 窃取されても早く期限切れ

// Refresh Token
// - 長い寿命(7日~30日)
// - Access Token更新時のみ使用
// - DBに保存して管理

// フロー
1. ログイン
Access Token(15) + Refresh Token(7) 発行

2. APIリクエスト
Access Token使用

3. Access Token期限切れ(15分後)
Refresh Tokenで新しいAccess Token取得

4. Refresh Tokenも期限切れ(7日後)
→ 再ログイン必要

// コード例
// トークン発行
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });

// 自動更新
if (accessTokenExpired) {
const newAccessToken = await refreshAccessToken(refreshToken);
}

🎓 次のステップ

JWTを理解したら、次を学習しましょう:

  1. HTTPSとは? (文書作成予定) - 安全なトークン送信
  2. CORSとは? (文書作成予定) - APIセキュリティ
  3. Cookieとは? - セッション vs JWT

便利なツール

// JWTデバッガー
// https://jwt.io
// - JWTデコード及び検証

// ライブラリ
// Node.js: jsonwebtoken
// Python: PyJWT
// Java: jjwt
// Go: jwt-go

// テスト
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
/*
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { userId: 123, ... },
signature: '...'
}
*/

🎬 まとめ

JWTは現代Webの標準認証方式です:

  • 構造: Header.Payload.Signature
  • 利点: Stateless、スケーラビリティ、様々なプラットフォーム対応
  • セキュリティ: HTTPS、HttpOnly Cookie、短い寿命
  • パターン: Access Token + Refresh Token

正しく使用すれば安全で効率的な認証システムを構築できます! 🎫✨