概要
Django REST Framework(DRF)をバックエンドに据えたシングルページアプリケーション(SPA)では、ブラウザの同一オリジンポリシーとCSRF保護をどう両立させるかが重要になる。単純に@csrf_exemptを付けるとセキュリティが低下するため、以下の2パターンで安全にトークンを付与する手法を紹介する。
パターンA:Cookieベースの自動付与
DRF標準のCSRF_USE_SESSIONS = False(デフォルト)のまま、JavaScript側でCookie値を読み取りリクエストヘッダへ注入する方法。
バックエンド設定(settings.py)
# settings.py
CSRF_COOKIE_HTTPONLY = False # JavaScriptから読み取りを許可
CSRF_COOKIE_SAMESITE = 'Lax' # 必要に応じて 'None' + Secureフラグ
フロントエンド実装(Vanilla JS)
// utils/csrf.js
export function readCookie(name) {
const match = document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`));
return match ? decodeURIComponent(match.split('=')[1]) : null;
}
// main.js
import ky from 'ky'; // fetchラッパー
const csrf = readCookie('csrftoken');
const api = ky.create({
prefixUrl: '/api/',
headers: {
'X-CSRFToken': csrf,
'Accept': 'application/json',
},
});
await api.post('items/', { json: { title: 'foo' } });
パターンB:動的トークン取得方式
認証フローがJWTやOAuth2主体で、Cookieを使わない場合に推奨。初回アクセス時に専用エンドポイントからCSRFトークンを取得し、以降の書き込みリクエストに付与する。
バックエンド(DRF ViewSet)
# views/csrf.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from django.middleware.csrf import get_token
@api_view(['GET'])
@permission_classes([AllowAny])
def csrf(request):
return Response({'csrf': get_token(request)})
ルーティング
# urls.py
from django.urls import path
from .views.csrf import csrf
urlpatterns = [
path('csrf/', csrf, name='csrf-token'),
]
フロントエンド(Axiosインターセプター)
// http/client.ts
import axios, { AxiosInstance } from 'axios';
class ApiClient {
private instance: AxiosInstance;
private csrfCache: string | null = null;
constructor() {
this.instance = axios.create({
baseURL: '/api/',
});
this.instance.interceptors.request.use(async (config) => {
if (['post', 'put', 'patch', 'delete'].includes(config.method!)) {
if (!this.csrfCache) {
const { data } = await axios.get('/csrf/');
this.csrfCache = data.csrf;
}
config.headers['X-CSRFToken'] = this.csrfCache;
}
return config;
});
}
async createItem(payload: { title: string }) {
const { data } = await this.instance.post('items/', payload);
return data;
}
}
export const api = new ApiClient();
セキュアな運用チェックリスト
- 本番環境ではHTTPS必須(SameSite=Noneを使う場合)
- トークンは常にサーバー生成・検証のみ許可
- JavaScript側でトークンをlocalStorageに永続化しない
- 認証が不要なGETリソースはCSRFチェックをスキップしても安全