🔑

Django × Nginx で実装するProtected Media機能のガイド

に公開

Django × Nginx で実装するProtected Media機能のガイド

はじめに

Webアプリケーションでユーザーがアップロードしたメディアファイル(音声・動画)を安全に配信するには、適切なアクセス制御が必要です。本記事では、Django 5.2とNginxを組み合わせて、認証されたユーザーのみがアクセス可能なProtected Media機能の実装方法を詳しく解説します。

この記事で学べること

  • Djangoでの認証ベースのメディアファイル配信
  • NginxのX-Accel-Redirectを使った効率的なファイル配信
  • ファイル名生成とストレージ管理
  • Docker環境でのメディア配信システム

使用技術

  • バックエンド: Django 5.2.7
  • Webサーバー: Nginx (Alpine)
  • コンテナ: Docker Compose
  • ファイルストレージ: カスタムFileSystemStorage
  • 認証: Django認証システム

Protected Mediaとは?

Protected Mediaとは、認証されたユーザーのみがアクセス可能なメディアファイルの配信機能です。一般的なWebサーバーの直接配信とは異なり、アプリケーション層でアクセス権限を制御します。

実装のメリット

  1. セキュリティの向上: 認証なしではファイルにアクセス不可

システム構成

全体アーキテクチャ

ディレクトリ構造

django-tailwindcss-auth/
├── app/
│   ├── models.py          # MediaFileモデル + SafeMediaFileStorage
│   ├── views.py           # protected_media関数
│   └── forms.py           # ファイルアップロードフォーム
├── config/
│   └── settings.py        # MEDIA設定
├── nginx.conf             # Nginx設定(X-Accel-Redirect)
├── docker-compose.yml     # コンテナ構成
└── media/                 # アップロードファイル格納

実装の詳細

1. カスタムファイルストレージの実装

安全なファイル名生成とストレージ管理を実装します。

# app/models.py
import os
import time
from django.core.files.storage import FileSystemStorage

class SafeMediaFileStorage(FileSystemStorage):
    """
    安全なメディアファイルストレージ
    """
    
    def get_available_name(self, name, max_length=None):
        """
        ファイル名を安全な形式に変換し、重複を避ける
        """
        # 絶対パスを相対パスに変換
        if os.path.isabs(name):
            name = os.path.basename(name)
        
        # ディレクトリとファイル名に分割
        dir_name = os.path.dirname(name)
        base_name = os.path.basename(name)
        safe_base_name = self._get_safe_filename(base_name)
        
        # ディレクトリが存在する場合は結合、そうでなければファイル名のみ
        safe_name = (
            os.path.join(dir_name, safe_base_name) if dir_name else safe_base_name
        )
        
        # 重複チェックのため元のget_available_nameメソッドを呼び出し
        return super().get_available_name(safe_name, max_length)
    
    def _get_safe_filename(self, filename):
        """
        ファイル名をタイムスタンプベースの安全な形式に変換
        """
        # ファイル拡張子を取得
        _, ext = os.path.splitext(filename)
        
        # タイムスタンプベースのファイル名を生成
        timestamp = int(time.time() * 1000)  # ミリ秒のタイムスタンプ
        
        # 安全なファイル名を生成
        safe_name = f"media_{timestamp}{ext}"
        
        return safe_name

実装のポイント:

  • セキュリティ: 元のファイル名を隠蔽してタイムスタンプベースに変換
  • 重複回避: get_available_nameでファイル名の重複を防止
  • 拡張子保持: 元のファイル形式を維持

2. メディアファイルモデルの実装

音声・動画ファイル用のモデルを定義します。

# app/models.py
class MediaFile(models.Model):
    """音声・動画ファイル用のモデル"""
    
    FILE_TYPE_CHOICES = [
        ("audio", "音声"),
        ("video", "動画"),
    ]
    
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ユーザー")
    title = models.CharField(max_length=200, verbose_name="タイトル")
    description = models.TextField(blank=True, verbose_name="説明")
    file_type = models.CharField(
        max_length=10, choices=FILE_TYPE_CHOICES, verbose_name="ファイル種別"
    )
    file = models.FileField(storage=SafeMediaFileStorage(), verbose_name="ファイル")
    file_size = models.PositiveIntegerField(verbose_name="ファイルサイズ(バイト)")
    duration = models.DurationField(null=True, blank=True, verbose_name="再生時間")
    created_at = models.DateTimeField(default=timezone.now, verbose_name="作成日時")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新日時")
    
    class Meta:
        verbose_name = "メディアファイル"
        verbose_name_plural = "メディアファイル"
        ordering = ["-created_at"]
    
    def get_file_size_mb(self):
        """ファイルサイズをMB単位で返す"""
        return round(self.file_size / (1024 * 1024), 2)
    
    def get_safe_filename(self):
        """安全なファイル名を取得"""
        if self.file:
            return os.path.basename(self.file.name)
        return ""

モデルの特徴:

  • ユーザー関連付け: ファイルの所有者を明確に管理
  • メタデータ管理: ファイルサイズ、再生時間などの情報を保存
  • 安全なファイル名: カスタムストレージによる安全なファイル管理

3. Protected Mediaビューの実装

認証チェックとNginx連携を実装します。

# app/views.py
import mimetypes
import os
from django.conf import settings
from django.http import Http404, HttpResponse
from django.contrib.auth.views import redirect_to_login

def protected_media(request, path):
    """保護されたメディアファイルへのアクセス制御"""
    user_authenticated = request.user.is_authenticated
    
    # ログイン済みユーザーは許可
    if user_authenticated:
        pass
    else:
        # 未認証ユーザーはログインページにリダイレクト
        return redirect_to_login(request.get_full_path())
    
    file_path = os.path.join(settings.MEDIA_ROOT, path)
    if not os.path.exists(file_path):
        raise Http404()
    
    response = HttpResponse()
    content_type, _ = mimetypes.guess_type(file_path)
    if content_type:
        response["Content-Type"] = content_type
    response["X-Accel-Redirect"] = f"/protected_media/{path}"
    return response

実装のポイント:

  • 認証チェック: request.user.is_authenticatedでログイン状態を確認
  • ファイル存在確認: os.path.exists()でファイルの存在を検証
  • X-Accel-Redirect: Nginxに内部リダイレクトを指示
  • MIME Type設定: 適切なContent-Typeを設定

4. Nginx設定の実装

X-Accel-Redirectを使った効率的なファイル配信を設定します。

# nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    
    # クライアント最大ボディサイズを設定(300MB)
    client_max_body_size 300M;
    
    server {
        listen 80;
        server_name _;
        
        # 静的ファイルの直接配信
        location /static/ {
            alias /staticfiles/;
        }
        
        # 保護されたメディアファイル(内部アクセスのみ)
        location /protected_media/ {
            internal;
            alias /media/;
        }
        
        # Djangoアプリケーションへのプロキシ
        location / {
            proxy_pass http://web:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Nginx設定のポイント:

  • internal: /protected_media/への直接アクセスを禁止
  • alias: ファイルシステムのパスをマッピング
  • client_max_body_size: 大容量ファイルアップロードに対応

5. URL設定

Protected Mediaのルーティングを設定します。

# app/urls.py
from django.urls import path
from . import views

app_name = "app"

urlpatterns = [
    # その他のURL...
    path("media/<path:path>", views.protected_media, name="protected_media"),
    # メディアファイル関連
    path("media-files/", views.MediaFileListView.as_view(), name="media_list"),
    path("media-files/upload/", views.MediaFileUploadView.as_view(), name="media_upload"),
    path("media-files/<int:pk>/", views.MediaFileDetailView.as_view(), name="media_detail"),
    path("media-files/<int:pk>/delete/", views.MediaFileDeleteView.as_view(), name="media_delete"),
]

6. Django設定

メディアファイルの設定を行います。

# config/settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Media files
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

Protected Mediaの動作フロー

1. ファイルアクセスの流れ

2. セキュリティの仕組み

  1. 認証チェック: Djangoでユーザーの認証状態を確認
  2. ファイル存在確認: リクエストされたファイルが存在するかチェック
  3. X-Accel-Redirect: Nginxに内部リダイレクトを指示
  4. 直接配信: Nginxがファイルを直接配信(Djangoをバイパス)

ファイルアップロード機能の実装

1. アップロードフォームの実装

# app/forms.py
class MediaFileUploadForm(forms.ModelForm):
    """メディアファイルアップロードフォーム"""
    
    class Meta:
        model = MediaFile
        fields = ["title", "description", "file_type", "file"]
        widgets = {
            "title": forms.TextInput(
                attrs={
                    "class": "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
                    "placeholder": "ファイルのタイトルを入力",
                }
            ),
            "description": forms.Textarea(
                attrs={
                    "class": "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
                    "rows": 3,
                    "placeholder": "ファイルの説明を入力(任意)",
                }
            ),
            "file_type": forms.Select(
                attrs={
                    "class": "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                }
            ),
            "file": forms.FileInput(
                attrs={
                    "class": "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
                    "accept": "audio/*,video/*",
                }
            ),
        }
    
    def clean_file(self):
        file = self.cleaned_data.get("file")
        if file:
            # ファイル形式の検証
            file_type = self.cleaned_data.get("file_type")
            if file_type == "audio":
                if not file.content_type.startswith("audio/"):
                    raise forms.ValidationError("音声ファイルを選択してください。")
            elif file_type == "video":
                if not file.content_type.startswith("video/"):
                    raise forms.ValidationError("動画ファイルを選択してください。")
        
        return file

2. アップロードビューの実装

# app/views.py
class MediaFileUploadView(LoginRequiredMixin, CreateView):
    """メディアファイルアップロードビュー"""
    
    model = MediaFile
    form_class = MediaFileUploadForm
    template_name = "multimedia/upload.html"
    success_url = reverse_lazy("app:media_list")
    login_url = "app:login"
    
    def form_valid(self, form):
        form.instance.user = self.request.user
        form.instance.file_size = form.instance.file.size
        return super().form_valid(form)
    
    def get_success_url(self):
        messages.success(self.request, "ファイルが正常にアップロードされました。")
        return super().get_success_url()

Docker環境での動作確認

1. Docker Compose設定

# docker-compose.yml
services:
  nginx:
    image: nginx:alpine
    container_name: django-tailwindcss-multimedia-auth-nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - staticfiles:/staticfiles
      - ./media:/media
    depends_on:
      - web
    networks:
      - django-tailwindcss-multimedia-auth-network
  web:
    build: .
    container_name: django-tailwindcss-multimedia-auth-web
    command: sh -c "uv run python manage.py migrate --noinput && uv run python manage.py collectstatic --noinput && uv run gunicorn config.asgi:application -k uvicorn_worker.UvicornWorker --bind 0.0.0.0:8000"
    volumes:
      - staticfiles:/app/staticfiles
      - ./media:/app/media
    env_file:
      - .env
    networks:
      - django-tailwindcss-multimedia-auth-network
  postgres:
    image: postgres:17
    container_name: django-tailwindcss-multimedia-auth-postgres
    restart: always
    env_file:
      - .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - django-tailwindcss-multimedia-auth-network
networks:
  django-tailwindcss-multimedia-auth-network:
    driver: bridge
volumes:
  staticfiles:
  postgres_data:

2. 起動手順

# コンテナ起動(マイグレーション・collectstatic・サーバー起動が自動実行)
docker compose up --build -d

# アクセス確認
curl http://localhost/

自動実行される処理:

  • データベースマイグレーション(uv run python manage.py migrate --noinput
  • 静的ファイルの収集(uv run python manage.py collectstatic --noinput
  • Gunicornサーバーの起動

まとめ

本記事では、Django 5.2とNginxを組み合わせたProtected Media機能の実装方法を詳しく解説しました。

実装した機能

  • ✅ 認証ベースのメディアファイル配信
  • ✅ 安全なファイル名生成
  • ✅ Nginx X-Accel-Redirectによる効率的な配信
  • ✅ ファイルアップロード・管理機能
  • ✅ Docker環境での本格運用

技術的な学び

  1. セキュリティ: 認証なしではファイルアクセス不可
  2. パフォーマンス: Nginxによる効率的なファイル配信
  3. スケーラビリティ: Docker環境での本格運用
  4. ユーザビリティ: 直感的なファイル管理インターフェース

Protected Mediaの特徴

  • 認証必須: ログインしていないユーザーはファイルにアクセス不可
  • 効率的な配信: NginxのX-Accel-Redirectによる高速配信
  • セキュアなファイル名: タイムスタンプベースの安全なファイル名
  • Docker対応: 本格運用環境での動作確認

Protected Media機能により、セキュアで効率的なメディアファイル配信システムを構築できます。本記事が、Djangoアプリケーションでのメディアファイル管理の参考になれば幸いです。

おわりに

この記事は筆者の学習過程で作成したものです。実装内容や説明に間違いや改善点があれば、ぜひご指摘いただけるとありがたいです。


参考リソース:

Discussion