JSON Web Tokens (JWT):完全ガイド
JWT認証の基礎、実装、セキュリティ。トークンベース認証システムの構築方法。
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# JSON Web Tokens (JWT):完全ガイド
JWT認証の仕組みを理解し、セキュアなアプリケーションを構築しましょう。
JWTとは
JSON Web Token (JWT) は、2つのパーティ間で情報を安全に伝送するためのコンパクトで自己完結型のトークンです。
構造
JWTは3つの部分から構成:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IueddOS4reWkquimjiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
分解:
HEADER.PAYLOAD.SIGNATURE
1. Header(ヘッダー)
{
"alg": "HS256",
"typ": "JWT"
}
- alg: 署名アルゴリズム(例:HS256, RS256)
- typ: トークンタイプ(JWT)
Base64URLエンコード:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload(ペイロード)
{
"sub": "1234567890",
"name": "田中太郎",
"iat": 1516239022,
"exp": 1516242622
}
標準クレーム:
- sub: Subject(ユーザーID)
- iat: Issued At(発行時刻)
- exp: Expiration(有効期限)
- iss: Issuer(発行者)
- aud: Audience(対象者)
Base64URLエンコード:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IueddOS4reWkquimjiIsImlhdCI6MTUxNjIzOTAyMn0
3. Signature(署名)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
なぜJWTか
従来のセッション vs JWT
セッションベース
クライアント サーバー
| |
|--ログイン要求----->|
| |
|<--セッションID-----|
| (Cookie) |
| [セッション保存]
| |
|--リクエスト------->|
| + Cookie [セッション検索]
| |
問題:
- サーバー側でセッション保存が必要
- スケールが難しい
- CORSの問題
JWTベース
クライアント サーバー
| |
|--ログイン要求----->|
| |
|<--JWT-------------|
| |
[JWTを保存] |
| |
|--リクエスト------->|
| + JWT [JWT検証]
| |
利点:
- ステートレス
- スケーラブル
- クロスドメイン対応
実装
Node.js / JavaScript
インストール
npm install jsonwebtoken
トークン生成
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your-secret-key';
// トークン生成
function generateToken(user) {
const payload = {
sub: user.id,
name: user.name,
email: user.email,
role: user.role
};
const options = {
expiresIn: '1h', // 1時間で期限切れ
issuer: 'my-app'
};
return jwt.sign(payload, SECRET_KEY, options);
}
// 使用例
const user = {
id: '123',
name: '田中太郎',
email: 'tanaka@example.com',
role: 'admin'
};
const token = generateToken(user);
console.log(token);
トークン検証
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
return { valid: true, data: decoded };
} catch (error) {
if (error.name === 'TokenExpiredError') {
return { valid: false, error: 'トークンの期限が切れています' };
}
if (error.name === 'JsonWebTokenError') {
return { valid: false, error: '無効なトークンです' };
}
return { valid: false, error: error.message };
}
}
// 使用
const result = verifyToken(token);
if (result.valid) {
console.log('ユーザー:', result.data);
} else {
console.error('エラー:', result.error);
}
Expressミドルウェア
const express = require('express');
const app = express();
// 認証ミドルウェア
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'トークンが必要です' });
}
const result = verifyToken(token);
if (!result.valid) {
return res.status(403).json({ error: result.error });
}
req.user = result.data;
next();
}
// ログインエンドポイント
app.post('/login', (req, res) => {
const { email, password } = req.body;
// ユーザー認証(実際のロジック)
const user = authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: '認証失敗' });
}
const token = generateToken(user);
res.json({ token });
});
// 保護されたルート
app.get('/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
app.listen(3000);
Python / Flask
インストール
pip install PyJWT Flask
実装
from flask import Flask, request, jsonify
import jwt
from datetime import datetime, timedelta
from functools import wraps
app = Flask(__name__)
SECRET_KEY = 'your-secret-key'
def generate_token(user):
"""JWTトークン生成"""
payload = {
'sub': user['id'],
'name': user['name'],
'email': user['email'],
'role': user['role'],
'exp': datetime.utcnow() + timedelta(hours=1),
'iat': datetime.utcnow()
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
def verify_token(token):
"""JWTトークン検証"""
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return {'valid': True, 'data': decoded}
except jwt.ExpiredSignatureError:
return {'valid': False, 'error': 'トークンの期限が切れています'}
except jwt.InvalidTokenError:
return {'valid': False, 'error': '無効なトークンです'}
def token_required(f):
"""認証デコレーター"""
@wraps(f)
def decorated(args, kwargs):
token = None
if 'Authorization' in request.headers:
auth_header = request.headers['Authorization']
try:
token = auth_header.split(' ')[1] # Bearer TOKEN
except IndexError:
return jsonify({'error': '無効なトークン形式'}), 401
if not token:
return jsonify({'error': 'トークンが必要です'}), 401
result = verify_token(token)
if not result['valid']:
return jsonify({'error': result['error']}), 403
return f(result['data'], args, *kwargs)
return decorated
@app.route('/login', methods=['POST'])
def login():
"""ログインエンドポイント"""
data = request.get_json()
email = data.get('email')
password = data.get('password')
# ユーザー認証(実際のロジック)
user = authenticate_user(email, password)
if not user:
return jsonify({'error': '認証失敗'}), 401
token = generate_token(user)
return jsonify({'token': token})
@app.route('/profile', methods=['GET'])
@token_required
def profile(current_user):
"""保護されたルート"""
return jsonify({'user': current_user})
if __name__ == '__main__':
app.run(debug=True)
クライアント側の実装
ブラウザ(Vanilla JS)
// ログイン
async function login(email, password) {
const response = await fetch('http://localhost:3000/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
// トークンをローカルストレージに保存
localStorage.setItem('token', data.token);
return true;
}
return false;
}
// 認証付きリクエスト
async function getProfile() {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:3000/profile', {
headers: {
'Authorization': Bearer ${token}
}
});
if (response.ok) {
return await response.json();
}
throw new Error('認証エラー');
}
// トークンのデコード(検証なし)
function decodeToken(token) {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('無効なトークン');
}
const payload = parts[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
}
// トークンの有効期限チェック
function isTokenExpired(token) {
try {
const decoded = decodeToken(token);
const exp = decoded.exp 1000; // ミリ秒に変換
return Date.now() >= exp;
} catch {
return true;
}
}
React
import { useState, useEffect } from 'react';
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
if (token && !isTokenExpired(token)) {
const userData = decodeToken(token);
setUser(userData);
}
setLoading(false);
}, []);
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const { token } = await response.json();
localStorage.setItem('token', token);
const userData = decodeToken(token);
setUser(userData);
return true;
}
return false;
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return { user, login, logout, loading };
}
// 使用
function App() {
const { user, login, logout } = useAuth();
if (user) {
return (
<div>
<h1>Welcome, {user.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
return <LoginForm onLogin={login} />;
}
リフレッシュトークン
実装
// サーバー側
function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, name: user.name },
ACCESS_SECRET,
{ expiresIn: '15m' } // 短い有効期限
);
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' } // 長い有効期限
);
// リフレッシュトークンをDBに保存
saveRefreshToken(user.id, refreshToken);
return { accessToken, refreshToken };
}
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'リフレッシュトークンが必要' });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// DBで確認
const isValid = await verifyRefreshToken(decoded.sub, refreshToken);
if (!isValid) {
return res.status(403).json({ error: '無効なリフレッシュトークン' });
}
// 新しいアクセストークン発行
const user = await getUser(decoded.sub);
const newAccessToken = jwt.sign(
{ sub: user.id, name: user.name },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(403).json({ error: '無効なリフレッシュトークン' });
}
});
クライアント側
// Axiosインターセプター
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
try {
const response = await axios.post('/refresh', { refreshToken });
const { accessToken } = response.data;
localStorage.setItem('token', accessToken);
originalRequest.headers['Authorization'] = Bearer ${accessToken};
return axios(originalRequest);
} catch {
// リフレッシュ失敗 - ログアウト
logout();
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
セキュリティのベストプラクティス
1. 秘密鍵の管理
// ❌ 悪い
const SECRET_KEY = 'mysecret';
// ✅ 良い
const SECRET_KEY = process.env.JWT_SECRET_KEY;
// より安全(RS256)
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
2. 適切な有効期限
// アクセストークン - 短く
{ expiresIn: '15m' }
// リフレッシュトークン - 長く
{ expiresIn: '7d' }
3. HTTPSの使用
// 常にHTTPSで送信
// HTTPでは盗聴の危険
4. 適切な保存
// ❌ 悪い - XSS脆弱
localStorage.setItem('token', token);
// ✅ 良い - HttpOnly Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 3600000 // 1時間
});
5. センシティブ情報を含めない
// ❌ 悪い
const payload = {
sub: user.id,
password: user.password, // 絶対にダメ
creditCard: user.card
};
// ✅ 良い
const payload = {
sub: user.id,
name: user.name,
role: user.role
};
6. アルゴリズムの検証
// アルゴリズムを明示的に指定
jwt.verify(token, secret, { algorithms: ['HS256'] });
// "none"アルゴリズムを拒否
よくある問題
1. トークンサイズが大きい
問題: ペイロードに多くのデータ 解決:// 最小限の情報のみ
const payload = {
sub: user.id,
role: user.role
};
// 詳細な情報は別途取得
2. トークンの無効化
問題: ログアウト後もトークンが有効 解決:// ブラックリスト(Redis)
const blacklist = new Set();
function logout(token) {
const decoded = jwt.decode(token);
blacklist.add(token);
// 有効期限後に削除
setTimeout(() => {
blacklist.delete(token);
}, (decoded.exp * 1000) - Date.now());
}
function verifyToken(token) {
if (blacklist.has(token)) {
throw new Error('無効なトークン');
}
return jwt.verify(token, SECRET_KEY);
}
まとめ
JWTの利点
- ✅ ステートレス
- ✅ スケーラブル
- ✅ クロスドメイン対応
- ✅ モバイルフレンドリー
- ✅ 標準化されている
使用すべき場合
- SPA(シングルページアプリ)
- モバイルアプリ
- マイクロサービス
- API認証
避けるべき場合
- セッション管理が簡単な場合
- 即座にトークンを無効化する必要がある
- 非常にセンシティブなデータ
JWTで、モダンで安全な認証システムを構築しましょう!