Django REST FrameworkでSPAが安全に通信するためのCSRF対策実装ガイド

概要

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チェックをスキップしても安全

タグ: Django REST Framework csrf Axios SPA セキュリティ

9月2日 06:26 投稿