2025年6月8日 星期日

從零開始使用 Waitress 搭配 Django 開發部落格功能

一、開發環境準備 (Development Environment Setup)

  1. Python 環境 (Python Environment)

    • 安裝 Python 3.8 或更新版本。建議使用 pyenvconda 來管理 Python 版本。
    • 創建並激活虛擬環境:
      Bash
      python -m venv venv
      source venv/bin/activate  # macOS/Linux
      .\venv\Scripts\activate   # Windows
      
  2. 安裝必要的套件 (Install Required Packages)

    Bash
    pip install django mysqlclient waitress beautifulsoup4 Pillow markdown
    
    • django: Django 框架本身。
    • mysqlclient: Django 連接 MySQL 的驅動。
    • waitress: 用於部署 Django 應用程式的 WSGI 伺服器。
    • beautifulsoup4: 如果您需要從富文本編輯器中清理 HTML 內容,這個庫很有用。
    • Pillow: 用於處理圖片,例如用戶頭像或文章封面圖。
    • markdown: 如果您希望用戶使用 Markdown 撰寫文章。
  3. MySQL 資料庫 (MySQL Database)

    • 安裝 MySQL 伺服器並創建一個資料庫,例如 myblog_db
    • 創建一個用戶並賦予其對 myblog_db 的權限。
    • 記下資料庫名稱、用戶名和密碼,稍後會在 Django 設定中使用。

Django 專案初始化 (Django Project Initialization)

  1. 創建 Django 專案 (Create Django Project)

    Bash
    django-admin startproject myblog_project .
    

    這會在當前目錄下創建 myblog_project 專案。

  2. 創建 Django 應用程式 (Create Django App)

    Bash
    python manage.py startapp blog
    

    這會創建一個名為 blog 的應用程式,用於實現部落格功能。

  3. 註冊應用程式 (Register App)

    打開 myblog_project/settings.py,在 INSTALLED_APPS 中添加 'blog':

    Python
    # myblog_project/settings.py
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'blog',  # Add your app here
    ]
    
  4. 配置資料庫 (Configure Database)

    在 myblog_project/settings.py 中,修改 DATABASES 配置以連接到 MySQL:

    Python
    # myblog_project/settings.py
    
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'myblog_db',  # 你的資料庫名稱
            'USER': 'your_mysql_user',  # 你的 MySQL 用戶名
            'PASSWORD': 'your_mysql_password',  # 你的 MySQL 密碼
            'HOST': 'localhost',  # 資料庫主機,通常是 localhost
            'PORT': '3306',       # MySQL 預設端口
            'OPTIONS': {
                'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
            }
        }
    }
    
  5. 設置靜態檔案 (Configure Static Files)

    在 myblog_project/settings.py 中添加或修改靜態檔案配置:

    Python
    # myblog_project/settings.py
    
    STATIC_URL = 'static/'
    STATIC_ROOT = BASE_DIR / 'staticfiles' # 部署時收集靜態檔案的路徑
    
    MEDIA_URL = 'media/'
    MEDIA_ROOT = BASE_DIR / 'media' # 用戶上傳檔案的路徑
    

    確保在 myblog_project/urls.py 中引入 STATIC_ROOTMEDIA_ROOT 的配置,以便在開發模式下提供服務:

    Python
    # myblog_project/urls.py
    
    from django.contrib import admin
    from django.urls import path, include
    from django.conf import settings
    from django.conf.urls.static import static
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('blog.urls')), # Include your app's URLs
    ]
    
    if settings.DEBUG:
        urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
        urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    
  6. 執行資料庫遷移 (Run Migrations)

    Bash
    python manage.py makemigrations
    python manage.py migrate
    

    這會創建 Django 內建應用程式和 blog 應用程式的資料庫表。

  7. 創建超級用戶 (Create Superuser)

    Bash
    python manage.py createsuperuser
    

    這將允許您訪問 Django 管理後台。

部落格應用程式開發 (Blog App Development)

  1. 定義模型 (Define Models) 打開 blog/models.py,定義 PostCategory 模型:

```python

# blog/models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.template.defaultfilters import slugify # 用於生成 URL 友好的 slug

class Category(models.Model):

name = models.CharField(max_length=100, unique=True)

slug = models1.SlugField(max_length=100, unique=True, blank=True)

    class Meta:
        verbose_name_plural = "Categories" # 在管理後台顯示的名稱

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.name

class Post(models.Model):

2 STATUS_CHOICES = (

('draft', '草稿'),

('published', '已發布'),

)

title = models.CharField(max_length=250)

slug = models.SlugField(max_length=250, unique_for_date='publish', blank=True)

author = models.ForeignKey(User, on_del3ete=models.CASCADE, related_name='blog_posts')

body = models.TextField()

publish = models.DateTimeField(default=timezone.now)

created = models.DateTimeField(auto_now_add=True)

updated = models.DateTimeField(auto_now=True)

status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

category = models.ForeignKey(Category, on_delete=models.SET4_NULL, null=True, blank=True)

# 可以添加圖片欄位,例如封面圖

# cover_image = models.ImageField(upload_to='post_covers/', blank=True, null=True)

    class Meta:
        ordering = ('-publish',) # 預設按發布日期倒序排列

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def __str__(self):

5 return self.title

    def get_absolute_url(self):
        return reverse('blog:post_detail', args=[self.publish.year,
                                                self.publish.month,
                                                self.publish.day,
                                                self.slug])

```

  * 重新執行資料庫遷移:
    ```bash
    python manage.py makemigrations blog
    python manage.py migrate
    ```
  1. 註冊模型到管理後台 (Register Models in Admin)

    打開 blog/admin.py:

    Python
    # blog/admin.py
    
    from django.contrib import admin
    from .models import Post, Category
    
    @admin.register(Category)
    class CategoryAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug',)
        prepopulated_fields = {'slug': ('name',)}
    
    @admin.register(Post)
    class PostAdmin(admin.ModelAdmin):
        list_display = ('title', 'slug', 'author', 'publish', 'status', 'category')
        list_filter = ('status', 'created', 'publish', 'author', 'category')
        search_fields = ('title', 'body')
        prepopulated_fields = {'slug': ('title',)}
        raw_id_fields = ('author',) # 使用 ID 選擇作者,適合大量用戶
        date_hierarchy = 'publish'
        ordering = ('status', 'publish')
    

    現在您可以訪問 http://127.0.0.1:8000/admin/ 並使用超級用戶登錄,開始管理部落格文章和分類。

  2. 定義視圖 (Define Views)

    打開 blog/views.py,定義部落格文章列表和詳情視圖:

    Python
    # blog/views.py
    
    from django.shortcuts import render, get_object_or_404
    from .models import Post, Category
    from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
    

6

def post_list(request, category_slug=None):

object_list = Post.objects.filter(status='published')

category = None

if category_slug:

category = get_object_or_404(Category, slug=category_slug)

object_list = object_list.filter(category=cate7gory)

    paginator = Paginator(object_list, 5) # 每頁顯示 5 篇文章
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        # 如果頁碼不是整數,則顯示第一頁
        posts = paginator.page(1)
    except EmptyPage:
        # 如果頁碼超出範圍 (例如,100 頁),則顯示最後一頁
        posts = paginator.page(paginator.num_pages)

    categories = Category.objects.all() # 獲取所有分類,用於導航
    return render(request, 'blog/post/list.html', {'page': page,
                                                  'posts': posts,
                                                  'category': category,
                                                  'categories': categories})

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, slug=post,
                                   status='published',
                                   publish__year=year,
                                   publish__month=month,
                                   publish__day=day)
    return render(request, 'blog/post/detail.html', {'post': post})
```
  1. 定義 URL 路由 (Define URL Routes)

    在 blog 應用程式目錄下創建 urls.py 檔案:

    Python
    # blog/urls.py
    
    from django.urls import path
    from . import views
    
    app_name = 'blog' # 定義應用程式命名空間
    
    urlpatterns = [
        # 文章列表
        path('', views.post_list, name='post_list'),
        path('category/<slug:category_slug>/', views.post_list, name='post_list_by_category'),
        # 文章詳情
        path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'),
    ]
    

    確保在 myblog_project/urls.py 中包含了 blog 應用程式的 URL(前面已經做過)。

整合 Bootstrap v5.0 (Integrate Bootstrap v5.0)

  1. 下載 Bootstrap 檔案 (Download Bootstrap Files)

    訪問 Bootstrap 官方網站 (https://getbootstrap.com/docs/5.0/) 下載編譯好的 CSS 和 JS 檔案。

  2. 創建靜態檔案目錄 (Create Static Files Directory)

    在 blog 應用程式目錄下創建 static/blog/css/ 和 static/blog/js/ 目錄。

    將 Bootstrap 的 bootstrap.min.css 放入 static/blog/css/。

    將 Bootstrap 的 bootstrap.bundle.min.js (包含 Popper) 放入 static/blog/js/。

  3. 創建基礎模板 (Create Base Template)

    在 blog 應用程式目錄下創建 templates/blog/base.html 檔案:

    HTML
    {% load static %}
    <!DOCTYPE html>
    <html lang="zh-Hant">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{% block title %}我的部落格{% endblock %}</title>
        <link href="{% static 'blog/css/bootstrap.min.css' %}" rel="stylesheet">
        <style>
            body { padding-top: 56px; } /* For fixed navbar */
        </style>
        {% block extra_css %}{% endblock %}
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
            <div class="container">
                <a class="navbar-brand" href="{% url 'blog:post_list' %}">我的部落格</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarNav">
                    <ul class="navbar-nav ms-auto">
                        <li class="nav-item">
                            <a class="nav-link active" aria-current="page" href="{% url 'blog:post_list' %}">首頁</a>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                分類
                            </a>
                            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                                {% for category in categories %}
                                    <li><a class="dropdown-item" href="{% url 'blog:post_list_by_category' category.slug %}">{{ category.name }}</a></li>
                                {% endfor %}
                                <li><hr class="dropdown-divider"></li>
                                <li><a class="dropdown-item" href="{% url 'blog:post_list' %}">所有文章</a></li>
                            </ul>
                        </li>
                        {% if user.is_authenticated %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'admin:index' %}">管理後台</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="#">登出</a> {# 實際登出功能需要您自己實現 #}
                            </li>
                        {% else %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'admin:index' %}">登入</a>
                            </li>
                        {% endif %}
                    </ul>
                </div>
            </div>
        </nav>
    
        <div class="container mt-4">
            {% block content %}
            {% endblock %}
        </div>
    
        <footer class="footer mt-auto py-3 bg-light">
            <div class="container text-center">
                <span class="text-muted">© 2025 我的部落格. All rights reserved.</span>
            </div>
        </footer>
    
        <script src="{% static 'blog/js/bootstrap.bundle.min.js' %}"></script>
        {% block extra_js %}{% endblock %}
    </body>
    </html>
    
    • 注意 {% load static %} 在模板頂部。
    • static 模板標籤用於生成靜態檔案的 URL。
  4. 創建文章列表模板 (Create Post List Template)

    在 blog/templates/blog/post/ 目錄下創建 list.html 檔案:

    HTML
    {% extends "blog/base.html" %}
    
    {% block title %}文章列表{% if category %} - {{ category.name }}{% endif %}{% endblock %}
    
    {% block content %}
        <h1>{% if category %}{{ category.name }} - {% endif %}最新文章</h1>
    
        {% for post in posts %}
            <div class="card mb-4">
                <div class="card-body">
                    <h2 class="card-title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
                    <p class="card-subtitle text-muted mb-2">
                        發布於 {{ post.publish|date:"Y年m月d日" }} by {{ post.author }}
                        {% if post.category %}
                            in <a href="{% url 'blog:post_list_by_category' post.category.slug %}">{{ post.category.name }}</a>
                        {% endif %}
                    </p>
                    <p class="card-text">{{ post.body|truncatechars:300|safe }}</p>
                    <a href="{{ post.get_absolute_url }}" class="btn btn-primary">閱讀更多 &raquo;</a>
                </div>
            </div>
        {% empty %}
            <p>目前沒有任何已發布的文章。</p>
        {% endfor %}
    
        {# 分頁導航 #}
        {% include "blog/pagination.html" with page=posts %}
    
    {% endblock %}
    
  5. 創建文章詳情模板 (Create Post Detail Template)

    在 blog/templates/blog/post/ 目錄下創建 detail.html 檔案:

    HTML
    {% extends "blog/base.html" %}
    {% load markdown_tags %} {# 如果您想用 markdown 呈現內容,需要自定義模板標籤 #}
    
    {% block title %}{{ post.title }}{% endblock %}
    
    {% block content %}
        <h1 class="mb-4">{{ post.title }}</h1>
        <p class="text-muted mb-3">
            發布於 {{ post.publish|date:"Y年m月d日" }} by {{ post.author }}
            {% if post.category %}
                in <a href="{% url 'blog:post_list_by_category' post.category.slug %}">{{ post.category.name }}</a>
            {% endif %}
        </p>
    
        <div class="blog-content mb-5">
            {# 如果您使用 markdown 格式撰寫文章,可以這樣顯示 #}
            {{ post.body|markdown|safe }}
            {# 否則直接顯示 #}
            {# {{ post.body|safe }} #}
        </div>
    
        <p><a href="{% url 'blog:post_list' %}" class="btn btn-secondary">&laquo; 返回文章列表</a></p>
    
        {# 您可以在這裡添加評論系統等功能 #}
    
    {% endblock %}
    
    • 關於 Markdown: 如果您想讓用戶使用 Markdown 撰寫文章,需要創建一個自定義模板標籤。 在 blog 應用程式目錄下創建 templatetags 資料夾,並在其中創建 __init__.pymarkdown_tags.py
      程式碼片段
      # blog/templatetags/markdown_tags.py
      from django import template
      from django.template.defaultfilters import stringfilter
      import markdown
      
      register = template.Library()
      
      @register.filter(name='markdown')
      @stringfilter
      def markdown_filter(value):
          return markdown.markdown(value, extensions=['extra', 'nl2br', 'sane_lists'])
      
      然後在模板中使用 {% load markdown_tags %} 並應用 |markdown 過濾器。
  6. 創建分頁模板 (Create Pagination Template)

    在 blog/templates/blog/ 目錄下創建 pagination.html 檔案:

    HTML
    {% if page.has_other_pages %}
        <nav aria-label="Page navigation">
            <ul class="pagination justify-content-center">
                {% if page.has_previous %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ page.previous_page_number }}" aria-label="Previous">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                {% else %}
                    <li class="page-item disabled">
                        <span class="page-link">&laquo;</span>
                    </li>
                {% endif %}
    
                {% for i in page.paginator.page_range %}
                    {% if page.number == i %}
                        <li class="page-item active" aria-current="page"><span class="page-link">{{ i }}</span></li>
                    {% else %}
                        <li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
                    {% endif %}
                {% endfor %}
    
                {% if page.has_next %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ page.next_page_number }}" aria-label="Next">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                {% else %}
                    <li class="page-item disabled">
                        <span class="page-link">&raquo;</span>
                    </li>
                {% endif %}
    

8 </ul>

</nav>

{% endif %}

```

測試開發伺服器 (Test Development Server)

在專案根目錄下運行:

Bash
python manage.py runserver

訪問 http://127.0.0.1:8000/,您應該能看到部落格文章列表。

部署到 Waitress (Deployment with Waitress)

Waitress 是一個生產級的 WSGI 伺服器,適合在 Windows 或非同步環境中部署 Django 應用程式。

  1. 收集靜態檔案 (Collect Static Files)

    在部署之前,需要收集所有靜態檔案到 STATIC_ROOT 指定的目錄:

    Bash
    python manage.py collectstatic
    

    這會將所有應用程式的靜態檔案(包括 Django admin 和 Bootstrap)複製到 staticfiles 目錄。

  2. 創建 Waitress 啟動腳本 (Create Waitress Launch Script)

    在專案根目錄下創建一個 run_waitress.py 檔案(或者您可以直接在命令行中執行):

    Python
    # run_waitress.py
    
    from waitress import serve
    from myblog_project.wsgi import application # 確保這裡是你的項目名.wsgi
    
    if __name__ == '__main__':
        print("Starting Waitress server on http://127.0.0.1:8000")
        serve(application, host='0.0.0.0', port=8000) # 0.0.0.0 允許從任何 IP 訪問
    
  3. 運行 Waitress 伺服器 (Run Waitress Server)

    Bash
    python run_waitress.py
    

    現在,您的 Django 部落格應用程式將由 Waitress 伺服器提供服務,運行在 http://127.0.0.1:8000 (或您配置的 IP 和端口)。

進一步的功能擴展 (Further Feature Expansion)

  • 評論系統 (Comments System)
    • 創建 Comment 模型,與 Post 關聯。
    • post_detail 視圖中處理評論的提交和顯示。
    • detail.html 中添加評論表單和評論列表。
    • 可以考慮使用 django-crispy-forms 讓表單更美觀。
  • 文章搜索 (Post Search)
    • views.py 中添加搜索邏輯,使用 Q 物件進行複雜查詢。
    • 在模板中添加搜索表單。
  • 用戶認證與授權 (User Authentication & Authorization)
    • Django 內建的 django.contrib.auth 已經提供了用戶認證功能。
    • 您可以實現用戶註冊、登錄、登出、密碼重置等功能。
    • 限制只有登錄用戶才能發布或編輯文章。
  • 富文本編輯器 (Rich Text Editor)
    • 集成 django-ckeditordjango-tinymce 等富文本編輯器,讓用戶可以更方便地撰寫文章。
  • 標籤系統 (Tagging System)
    • 使用 django-taggit 等庫為文章添加標籤。
  • 響應式設計優化 (Responsive Design Optimization)
    • Bootstrap 已經是響應式的,但您可能需要根據自己的設計進一步調整 CSS。
  • SEO 優化 (SEO Optimization)
    • 為文章添加 meta 描述、關鍵字等。
    • 生成 sitemap。
  • 安全考慮 (Security Considerations)
    • 確保 DEBUG = False 在生產環境中。
    • 配置 SECRET_KEY 為一個複雜且保密的值。
    • 使用 HTTPS。
    • 處理用戶輸入的數據,防止 XSS、SQL 注入等攻擊。

總結

這個指南為您提供了一個從零開始使用 Django、MySQL、Waitress 和 Bootstrap 5 開發部落格功能的完整路線圖。請記住,這是一個基礎框架,您需要根據自己的需求進行深入開發和功能擴展。在開發過程中,遇到問題時,查閱 Django 官方文檔、Bootstrap 官方文檔和相關社區資源是非常有幫助的。


二、關於 Django 搭配 Waitress 和 MySQL 開發部落格,在效能和安全性方面的設定是至關重要的。以下將提供詳細的建議和配置範例。

效能設定 (Performance Settings)

效能優化通常涉及多個層面,從資料庫到程式碼,再到伺服器配置。

1. 資料庫優化 (MySQL)

  • 索引 (Indexes):

    • 確保您的模型中,常用於查詢條件的欄位(如 Post 模型中的 publishstatusslugCategory 模型中的 nameslug)都建立了索引。Django 在定義 ForeignKey, unique=Truedb_index=True 時會自動建立索引。
    • 對於 Post 模型中的 publishslug,由於您在 get_absolute_urlpost_detail 視圖中會同時使用,unique_for_date='publish' 已經暗示了複合索引,但仍然可以手動確認或添加。
    • 範例 (blog/models.py):
      Python
      class Post(models.Model):
          # ...
          slug = models.SlugField(max_length=250, unique_for_date='publish', db_index=True) # 確保有索引
          publish = models.DateTimeField(default=timezone.now, db_index=True) # 確保有索引
          status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft', db_index=True) # 確保有索引
          category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, db_index=True)
          # ...
      
    • 使用 EXPLAIN: 在開發環境中使用 MySQL 的 EXPLAIN 語句來分析您的查詢,確保它們正在有效利用索引。
  • 連接池 (Connection Pooling):

    • 對於高流量的應用,頻繁地建立和關閉資料庫連接會產生開銷。可以使用資料庫連接池,例如 django-db-connection-pool
    • 安裝: pip install django-db-connection-pool
    • settings.py 配置:
      Python
      # myblog_project/settings.py
      DATABASES = {
          'default': {
              'ENGINE': 'dj_db_conn_pool.backends.mysql', # 修改這裡
              'NAME': 'myblog_db',
              'USER': 'your_mysql_user',
              'PASSWORD': 'your_mysql_password',
              'HOST': 'localhost',
              'PORT': '3306',
              'OPTIONS': {
                  'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
              },
              'CONN_MAX_AGE': 600, # 連接在池中保持活躍的最長時間 (秒)
          }
      }
      
    • 這可以顯著減少資料庫連接的開銷。
  • 緩存查詢 (Caching Queries):

    • 對於不經常變動但頻繁查詢的數據,例如分類列表,可以使用 Django 的緩存框架。
    • settings.py 配置緩存後端:
      Python
      # myblog_project/settings.py
      CACHES = {
          'default': {
              'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 內存緩存,適合小型應用
              'LOCATION': 'unique-snowflake',
          }
          # 如果需要更高效的緩存,可以使用 Redis 或 Memcached
          # 'default': {
          #     'BACKEND': 'django_redis.cache.RedisCache',
          #     'LOCATION': 'redis://127.0.0.1:6379/1',
          #     'OPTIONS': {
          #         'CLIENT_CLASS': 'django_redis.client.DefaultClient',
          #     }
          # }
      }
      
    • 在視圖中使用緩存:
      Python
      # blog/views.py
      from django.core.cache import cache
      
      def post_list(request, category_slug=None):
          # ... (其他代碼)
      
          categories = cache.get('all_categories')
          if not categories:
              categories = Category.objects.all()
              cache.set('all_categories', categories, 60 * 60) # 緩存 1 小時
      
          return render(request, 'blog/post/list.html', {
              # ...
              'categories': categories
          })
      

2. Django ORM 優化

  • 使用 select_relatedprefetch_related:

    • 當您查詢一個模型時,如果需要訪問其相關聯的外鍵或多對多關係的數據,使用這些方法可以避免 N+1 查詢問題,從而大大減少資料庫查詢次數。
    • 範例 (blog/views.py):
      Python
      def post_list(request, category_slug=None):
          object_list = Post.objects.filter(status='published').select_related('author', 'category') # 預先加載 author 和 category
          # ...
      
      這樣在模板中訪問 post.author.usernamepost.category.name 時就不會觸發額外的資料庫查詢。
  • 限制查詢結果 (Limiting QuerySets):

    • 只獲取您需要的數據。如果只需要某幾個欄位,可以使用 only()defer()
    • Post.objects.only('title', 'slug', 'publish')
  • 分頁 (Pagination):

    • 您已經在 post_list 視圖中實現了分頁,這是處理大量數據集的標準做法,可以避免一次性加載所有數據導致的記憶體和時間開銷。

3. 靜態檔案和媒體檔案 (Static and Media Files)

  • 部署時的靜態檔案服務 (Production Static File Serving):

    • 不要讓 Django 在生產環境中服務靜態檔案。Waitress 是一個 WSGI 伺服器,它不擅長直接服務靜態檔案。
    • 在生產環境中,應該使用專門的 Web 伺服器(如 Nginx 或 Apache)來服務靜態檔案和媒體檔案。
    • Nginx 配置範例:
      Nginx
      server {
          listen 80;
          server_name your_domain.com;
      
          location /static/ {
              alias /path/to/your/myblog_project/staticfiles/; # 替換為你的 STATIC_ROOT
          }
      
          location /media/ {
              alias /path/to/your/myblog_project/media/; # 替換為你的 MEDIA_ROOT
          }
      
          location / {
              proxy_pass http://127.0.0.1:8000; # Waitress 監聽的地址和端口
              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;
          }
      }
      
    • 在部署前執行 python manage.py collectstatic 來收集所有靜態檔案到 STATIC_ROOT
  • CDN (Content Delivery Network):

    • 對於面向全球用戶的網站,使用 CDN 可以加速靜態檔案的載入,減少伺服器負載。

4. Waitress 配置

  • 調整線程數 (Threads):

    • Waitress 預設使用 4 個線程。您可以根據伺服器資源和預期的併發量進行調整。
    • run_waitress.py:
      Python
      from waitress import serve
      from myblog_project.wsgi import application
      
      if __name__ == '__main__':
          print("Starting Waitress server on http://127.0.0.1:8000")
          # 調整為更多的線程數,例如 8 或 16,但不要超過 CPU 核數太多
          serve(application, host='0.0.0.0', port=8000, threads=8)
      
    • 過多的線程可能會導致上下文切換開銷過大,過少則無法充分利用 CPU。需要根據實際測試進行調整。
  • 日誌 (Logging):

    • Waitress 預設日誌級別為 INFO,這可能會產生大量日誌。在生產環境中,可以考慮調整日誌級別,或將日誌輸出到檔案而不是控制台。
    • settings.py:
      Python
      # myblog_project/settings.py
      LOGGING = {
          'version': 1,
          'disable_existing_loggers': False,
          'handlers': {
              'file': {
                  'level': 'INFO', # 或 'WARNING', 'ERROR'
                  'class': 'logging.FileHandler',
                  'filename': '/path/to/your/logs/waitress.log', # 設定日誌檔案路徑
              },
          },
          'loggers': {
              'waitress': {
                  'handlers': ['file'],
                  'level': 'INFO',
                  'propagate': False,
              },
          },
      }
      

安全性設定 (Security Settings)

安全性是任何 Web 應用程式的基石。以下是針對 Django、Waitress 和 MySQL 的關鍵安全建議。

1. Django 核心安全設定

  • SECRET_KEY (settings.py):

    • 這是 Django 專案中最重要的安全設定之一。用於簽名加密、會話管理等。
    • 絕不能暴露給他人
    • 在生產環境中,應從環境變數中讀取,而不是直接寫在程式碼中。
    • 範例:
      Python
      # myblog_project/settings.py
      import os
      
      SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'your-very-long-and-complex-secret-key-for-development') # 開發用預設值
      
      # 在生產環境中,確保設置環境變數:
      # export DJANGO_SECRET_KEY='實際的超長隨機密鑰'
      
    • 生成一個強大的 SECRET_KEY 可以使用 Python 程式碼:
      Python
      from django.core.management.utils import get_random_secret_key
      print(get_random_secret_key())
      
  • DEBUG = False (settings.py):

    • 生產環境中必須設置為 False
    • DEBUGTrue 時,Django 會顯示詳細的錯誤訊息,這可能暴露敏感資訊給攻擊者。它還會禁用一些安全檢查。
    • 範例:
      Python
      # myblog_project/settings.py
      DEBUG = os.environ.get('DJANGO_DEBUG', 'True') == 'True' # 可以通過環境變數控制
      
      # 在生產環境中: export DJANGO_DEBUG='False'
      
  • ALLOWED_HOSTS (settings.py):

    • DEBUG = False 時,Django 會強制檢查 ALLOWED_HOSTS 設定。這可以防止 HTTP Host Header 攻擊。
    • 必須列出允許訪問您的網站的域名或 IP 地址。
    • 範例:
      Python
      # myblog_project/settings.py
      ALLOWED_HOSTS = ['your_domain.com', 'www.your_domain.com', 'your_server_ip']
      # 或者從環境變數讀取
      # ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')
      
  • CSRF 防護 (CSRF Protection):

    • Django 內建了強大的 CSRF (Cross-Site Request Forgery) 保護,通過 CsrfViewMiddleware 自動啟用。
    • 確保在所有表單中包含 {% csrf_token %} 模板標籤。
    • 範例 (在任何 POST 表單中):
      HTML
      <form method="post">
          {% csrf_token %}
          <button type="submit">Submit</button>
      </form>
      
  • XSS 防護 (XSS Protection):

    • Django 模板會自動轉義 HTML 內容,防止 XSS (Cross-Site Scripting) 攻擊。
    • 除非您明確知道自己在做什麼,否則不要使用 |safe 過濾器。
    • 如果您確實需要顯示用戶提交的 HTML 內容(例如富文本編輯器),請務必使用一個成熟的 HTML 清理庫(如 bleachBeautifulSoup)來過濾掉惡意標籤和屬性。
    • 範例 (清理用戶提交的 HTML):
      Python
      # 在您的視圖或模型 save 方法中
      from bs4 import BeautifulSoup
      import bleach
      
      def clean_html_content(html_content):
          allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
                          'em', 'i', 'li', 'ol', 'p', 'strong', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br']
          allowed_attrs = {'a': ['href', 'title']}
      
          soup = BeautifulSoup(html_content, 'html.parser')
          cleaned_html = bleach.clean(str(soup), tags=allowed_tags, attributes=allowed_attrs, strip=True)
          return cleaned_html
      
      # 在保存 Post.body 之前調用 clean_html_content
      
      安裝 bleach: pip install bleach
  • SQL 注入防護 (SQL Injection Protection):

    • Django ORM 已經內建了 SQL 注入防護,只要您使用 ORM 提供的查詢方法,而不是手動拼接 SQL 語句。
    • 避免使用 raw()extra() 方法拼接用戶輸入。如果非要使用,請務必使用參數化查詢。
  • 會話管理 (Session Management):

    • 使用 Django 內建的會話框架。
    • 確保會話 Cookie 設置了 securehttponly 標誌。
    • settings.py:
      Python
      # myblog_project/settings.py
      SESSION_COOKIE_SECURE = True   # 僅在 HTTPS 連接上發送會話 Cookie
      SESSION_COOKIE_HTTPONLY = True # 防止 JavaScript 訪問會話 Cookie
      CSRF_COOKIE_SECURE = True      # 僅在 HTTPS 連接上發送 CSRF Cookie
      CSRF_COOKIE_HTTPONLY = True    # 防止 JavaScript 訪問 CSRF Cookie
      
  • 密碼哈希 (Password Hashing):

    • Django 預設使用強大的密碼哈希算法(如 PBKDF2 with SHA256)。
    • 不要更改預設值,除非您有非常充分的理由。
  • 管理後台安全 (Admin Security):

    • 定期更新管理後台的超級用戶密碼。
    • 限制管理後台的訪問 IP(可以通過 Web 伺服器配置)。
    • 使用兩因素驗證 (Two-Factor Authentication, 2FA) 可以極大地增強管理後台的安全性(例如 django-two-factor-auth)。
  • 自定義用戶模型 (Custom User Model):

    • 如果您的應用程式需要擴展用戶模型,建議在專案啟動時就定義一個自定義用戶模型,即使它只是繼承了 AbstractUser。這樣可以避免將來擴展時的複雜性。
    • settings.py:
      Python
      AUTH_USER_MODEL = 'yourapp.CustomUser' # 例如 'users.User'
      

2. MySQL 資料庫安全

  • 獨立用戶 (Dedicated User):

    • 為 Django 應用程式創建一個專用的 MySQL 用戶,並只賦予其必要的權限(例如 SELECT, INSERT, UPDATE, DELETE 到指定的資料庫),而不是 rootALL PRIVILEGES
    • 範例:
      SQL
      CREATE USER 'your_django_user'@'localhost' IDENTIFIED BY 'your_secure_password';
      GRANT SELECT, INSERT, UPDATE, DELETE ON myblog_db.* TO 'your_django_user'@'localhost';
      FLUSH PRIVILEGES;
      
  • 強密碼 (Strong Passwords):

    • 為所有資料庫用戶設置強大且複雜的密碼。
  • 限制遠端連接 (Limit Remote Access):

    • 如果您的資料庫伺服器和應用程式伺服器在同一台機器上,將 MySQL 配置為只監聽 localhostbind-address = 127.0.0.1my.cnfmy.ini 中)。
    • 如果需要遠端連接,請限制允許連接的 IP 地址。

3. Waitress 和伺服器安全

  • 使用 Nginx/Apache 作為反向代理 (Reverse Proxy):

    • 如前所述,不應直接將 Waitress 暴露給公網。使用 Nginx 或 Apache 作為反向代理,它們提供了更好的安全性、日誌記錄、限流和 SSL/TLS 終止功能。
    • SSL/TLS (HTTPS):
      • 這是最重要的安全措施之一。所有生產網站都應該使用 HTTPS。
      • 使用 Let's Encrypt 等工具獲取免費的 SSL 證書,並在 Nginx/Apache 中配置。
      • Nginx SSL 配置範例:
        Nginx
        server {
            listen 443 ssl;
            server_name your_domain.com;
        
            ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
            ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
        
            ssl_protocols TLSv1.2 TLSv1.3;
            ssl_prefer_server_ciphers on;
            ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
            ssl_ecdh_curve secp384r1; # Nginx >= 1.7.0
            ssl_session_cache shared:SSL:10m;
            ssl_session_timeout 1d;
            ssl_session_tickets off;
            ssl_stapling on;
            ssl_stapling_verify on;
            resolver 8.8.8.8 8.8.4.4 valid=300s;
            resolver_timeout 5s;
            add_header X-Frame-Options DENY;
            add_header X-Content-Type-Options nosniff;
            add_header X-XSS-Protection "1; mode=block";
        
            location /static/ {
                alias /path/to/your/myblog_project/staticfiles/;
            }
            location /media/ {
                alias /path/to/your/myblog_project/media/;
            }
        
            location / {
                proxy_pass http://127.0.0.1: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;
            }
        }
        
        # HTTP to HTTPS redirect
        server {
            listen 80;
            server_name your_domain.com;
            return 301 https://$host$request_uri;
        }
        
        • X-Frame-Options: DENY: 防止點擊劫持攻擊。
        • X-Content-Type-Options: nosniff: 防止瀏覽器 MIME 類型嗅探。
        • X-XSS-Protection: 1; mode=block: 啟用瀏覽器內建的 XSS 過濾器。
  • 防火牆 (Firewall):

    • 在伺服器層級設置防火牆(如 ufw on Linux),只允許必要的端口(SSH, HTTP, HTTPS)對外開放。Waitress 監聽的端口 (如 8000) 應該只允許來自反向代理的連接。
  • 系統更新 (System Updates):

    • 定期更新您的操作系統、Python、Django 和所有相關庫,以修補已知的安全漏洞。
  • 日誌監控 (Log Monitoring):

    • 定期檢查 Django、Web 伺服器和 Waitress 的日誌,以便及早發現潛在的安全問題或異常行為。
  • 生產環境的環境變數 (Production Environment Variables):

    • 將所有敏感資訊(如 SECRET_KEY、資料庫密碼)存儲在環境變數中,而不是硬編碼在 settings.py 中。
    • 可以使用 python-decoupledjango-environ 等庫來更方便地管理環境變數。
  • 入侵檢測系統 (Intrusion Detection Systems):

    • 考慮使用像 Fail2Ban 這樣的工具來監控日誌文件並自動封鎖惡意 IP 地址,例如針對 SSH 暴力破解或 Web 應用攻擊。

總之,效能和安全性是持續的過程,沒有一勞永逸的解決方案。您需要定期審查您的配置、更新您的軟體並監控您的系統,以確保您的部落格安全高效地運行。

沒有留言:

張貼留言

網誌存檔