🎫 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);
});