← ブログに戻る

JSON Web Tokens (JWT):完全ガイド

JWT認証の基礎、実装、セキュリティ。トークンベース認証システムの構築方法。

Big JSON Team15 分で読めますapi
B

Big JSON Team

Technical Writer

Expert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.

15 分読む

# 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で、モダンで安全な認証システムを構築しましょう!

Share:

関連記事

Read in English