Django 框架在设计时就考虑了安全性,提供了许多内置的安全特性。然而,正确配置和使用这些特性至关重要。本指南将介绍 Django 的安全最佳实践,帮助您构建安全的 Web 应用程序。
安全是一个持续的过程,不是一次性任务。始终遵循最小权限原则,定期更新依赖,并进行安全审计。
| 攻击类型 | 风险等级 | Django 防护 |
|---|---|---|
| SQL 注入 | 严重 | 内置 ORM 防护 |
| XSS | 高 | 模板自动转义 |
| CSRF | 高 | CSRF 中间件 |
| 点击劫持 | 中 | X-Frame-Options |
| 会话劫持 | 高 | 安全的会话管理 |
正确的 Django 配置是安全的基础。以下是在 settings.py 中必须配置的安全设置。
import os
from pathlib import Path
# 生产环境必须设置为 False
DEBUG = False
# 允许的主机列表
ALLOWED_HOSTS = [
'yourdomain.com',
'www.yourdomain.com',
'127.0.0.1', # 仅在开发时使用
]
# 安全密钥 - 生产环境使用环境变量
SECRET_KEY = os.environ.get('SECRET_KEY', 'fallback-secret-key-for-dev-only')
# HTTPS 设置
SECURE_SSL_REDIRECT = True # 强制 HTTPS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True # 仅通过 HTTPS 传输会话 Cookie
CSRF_COOKIE_SECURE = True # 仅通过 HTTPS 传输 CSRF Cookie
# 安全头设置
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY' # 防止点击劫持
# HSTS 设置 (谨慎使用)
SECURE_HSTS_SECONDS = 31536000 # 1年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
| 设置 | 描述 | 风险等级 |
|---|---|---|
DEBUG |
生产环境必须为 False,避免信息泄露 | 严重 |
ALLOWED_HOSTS |
限制可服务的主机,防止主机头攻击 | 高 |
SECRET_KEY |
使用强密钥,生产环境使用环境变量 | 严重 |
SECURE_SSL_REDIRECT |
强制所有流量使用 HTTPS | 高 |
X_FRAME_OPTIONS |
防止点击劫持攻击 | 中 |
import os
# 环境检测
ENVIRONMENT = os.environ.get('DJANGO_ENV', 'development')
if ENVIRONMENT == 'production':
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
SECURE_SSL_REDIRECT = True
# 其他生产环境安全设置...
else:
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# 开发环境设置...
跨站请求伪造 (CSRF) 攻击迫使登录用户在不知情的情况下执行非预期的操作。Django 提供了内置的 CSRF 保护机制。
MIDDLEWARE = [
# ...
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
<form method="post">
{% csrf_token %}
<!-- 表单字段 -->
<input type="text" name="username">
<button type="submit">提交</button>
</form>
{# 对于 AJAX 请求 #}
<script>
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/endpoint/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken,
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
</script>
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.utils.decorators import method_decorator
from django.views.generic import View
# 需要 CSRF 保护的视图(默认)
def normal_view(request):
if request.method == 'POST':
# CSRF 保护自动生效
pass
# 豁免 CSRF 保护(谨慎使用)
@csrf_exempt
def exempt_view(request):
# 这个视图不受 CSRF 保护
pass
# 类视图的 CSRF 保护
class MyView(View):
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
{% csrf_token %}@csrf_exempt,仅在必要时使用CsrfViewMiddleware 在中间件中启用跨站脚本攻击 (XSS) 允许攻击者在受害者浏览器中执行恶意脚本。Django 模板系统提供自动转义来防止 XSS 攻击。
{# 自动转义 - 安全 #}
<p>{{ user_input }}</p>
{# 输出: <script>alert('XSS')</script> #}
{# 手动关闭转义 - 危险! #}
<p>{{ user_input|safe }}</p>
{# 输出: <script>alert('XSS')</script> #}
{# 条件性标记安全 #}
<p>{{ user_input|escape }}</p>
{# 显式转义 #}
{# 安全的HTML内容 #}
{% autoescape off %}
<p>{{ trusted_html_content }}</p>
{% endautoescape %}
# 安装: pip install django-bleach
# forms.py
from django import forms
import bleach
class CommentForm(forms.Form):
content = forms.CharField(widget=forms.Textarea)
def clean_content(self):
content = self.cleaned_data['content']
# 清理 HTML,只允许安全的标签和属性
cleaned_content = bleach.clean(
content,
tags=['p', 'br', 'strong', 'em', 'a'],
attributes={'a': ['href', 'title']},
strip=True
)
return cleaned_content
|safe 过滤器# 安装
pip install django-csp
# settings.py
MIDDLEWARE = [
# ...
'csp.middleware.CSPMiddleware',
]
# CSP 配置
CSP_DEFAULT_SRC = ["'self'"]
CSP_SCRIPT_SRC = ["'self'", "https://cdn.example.com"]
CSP_STYLE_SRC = ["'self'", "'unsafe-inline'"]
CSP_IMG_SRC = ["'self'", "data:", "https:"]
CSP_FONT_SRC = ["'self'"]
Django 的 ORM 使用参数化查询,从根本上防止了 SQL 注入攻击。但不当使用仍然可能导致安全问题。
from django.db import models
from django.contrib.auth.models import User
# 安全 - 使用 ORM
users = User.objects.filter(username=request.GET['username'])
# 安全 - 参数化查询
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE username = %s", [username])
results = cursor.fetchall()
# 危险 - 字符串拼接(绝对避免!)
dangerous_sql = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(dangerous_sql)
# 安全的原始查询
class UserManager(models.Manager):
def search_users(self, search_term):
return self.raw(
'SELECT * FROM auth_user WHERE username LIKE %s',
[f'%{search_term}%'] # 参数化,安全
)
# 危险 - 直接字符串插值
def dangerous_search(search_term):
return User.objects.raw(
f"SELECT * FROM auth_user WHERE username LIKE '%{search_term}%'"
) # SQL 注入漏洞!
from django.db.models import Q
# 使用 Q 对象进行复杂查询
def advanced_search(request):
query = request.GET.get('q', '')
filters = Q()
if query:
filters |= Q(username__icontains=query)
filters |= Q(email__icontains=query)
filters |= Q(first_name__icontains=query)
return User.objects.filter(filters)
# 限制查询结果
def get_user_profile(request, user_id):
# 确保用户只能访问自己的资料
if request.user.id != user_id:
raise PermissionDenied
return User.objects.get(id=user_id)
# 使用 select_related 和 prefetch_related 优化查询
articles = Article.objects.select_related('author').prefetch_related('tags')
extra() 和 RawSQLDjango 提供了强大的认证系统,但正确配置和使用至关重要。
# settings.py
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 8,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# 密码哈希设置
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
]
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
# 要求登录
@login_required
def profile_view(request):
return render(request, 'profile.html')
# 要求特定权限
@permission_required('app.view_sensitive_data')
def sensitive_view(request):
return render(request, 'sensitive.html')
# 基于类的视图权限
class AdminView(LoginRequiredMixin, PermissionRequiredMixin, View):
permission_required = 'app.admin_access'
login_url = '/login/'
redirect_field_name = 'next'
# settings.py
# 会话设置
SESSION_COOKIE_AGE = 1209600 # 2周,默认
SESSION_COOKIE_SECURE = True # 仅 HTTPS
SESSION_COOKIE_HTTPONLY = True # 防止 XSS 读取
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF 防护
# 会话引擎(推荐使用数据库或缓存)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
# 或者使用缓存
# SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# 会话过期设置
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 持久会话
SESSION_SAVE_EVERY_REQUEST = True # 每次请求保存会话
HttpOnly 标志
# 使特定会话失效
from django.contrib.sessions.models import Session
def logout_everywhere(request):
# 删除当前会话以外的所有会话
Session.objects.exclude(
session_key=request.session.session_key
).delete()
文件上传功能容易受到多种攻击,必须实施严格的安全措施。
import os
from django.core.files.storage import FileSystemStorage
from django.core.exceptions import ValidationError
def validate_file_extension(value):
ext = os.path.splitext(value.name)[1]
valid_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.png', '.txt']
if not ext.lower() in valid_extensions:
raise ValidationError('不支持的文件类型')
def validate_file_size(value):
filesize = value.size
if filesize > 5 * 1024 * 1024: # 5MB
raise ValidationError("文件大小不能超过5MB")
class SecureFileForm(forms.Form):
file = forms.FileField(
validators=[validate_file_extension, validate_file_size],
widget=forms.FileInput(attrs={
'accept': '.pdf,.doc,.docx,.jpg,.png,.txt'
})
)
# settings.py
# 媒体文件安全设置
MEDIA_ROOT = '/var/www/media/' # Web 根目录之外
MEDIA_URL = '/media/'
# 使用自定义存储后端
DEFAULT_FILE_STORAGE = 'myapp.storage.SecureFileStorage'
# 或者在视图级别控制
from django.core.files.storage import FileSystemStorage
from django.http import HttpResponseForbidden
class SecureStorage(FileSystemStorage):
def get_available_name(self, name, max_length=None):
# 防止目录遍历
name = os.path.basename(name)
return super().get_available_name(name, max_length)
def secure_file_download(request, file_path):
# 验证用户权限
if not request.user.has_perm('app.download_file'):
return HttpResponseForbidden()
# 防止路径遍历
file_path = os.path.basename(file_path)
# 提供文件下载...
DEBUG = False
包含所有有效域名
SECRET_KEY 不在代码中硬编码
所有流量强制使用 SSL/TLS
使用强密码,限制网络访问
使用 pip audit 检查漏洞
# 检查安全配置
python manage.py check --deploy
# 检查依赖漏洞
pip audit
pip list --outdated
# 使用 safety 检查已知漏洞
pip install safety
safety check
# 使用 bandit 进行代码安全扫描
pip install bandit
bandit -r myproject/
# 使用 django-extensions 进行安全检查
python manage.py validate_templates
python manage.py show_urls
# requirements-dev.txt
django-debug-toolbar==3.2.4
django-extensions==3.1.3
bandit==1.7.0
safety==1.10.3
pytest-bandit==0.6.1
# 在 CI/CD 中添加安全扫描
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Bandit
run: |
pip install bandit
bandit -r myproject/ -f html -o bandit_report.html
- name: Run Safety
run: |
pip install safety
safety check --json > safety_report.json
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'security_file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/security.log',
},
},
'loggers': {
'django.security': {
'handlers': ['security_file'],
'level': 'WARNING',
'propagate': False,
},
},
}
安全不仅仅是技术问题,更是文化问题。建立安全意识,进行安全培训,让每个团队成员都成为应用安全的第一道防线。