Django 安全最佳实践
2026/3/20大约 6 分钟
Django 安全最佳实践
一、常见安全威胁
1.1 OWASP Top 10
1. 注入攻击(SQL注入、命令注入)
2. 失效的身份认证
3. 敏感数据泄露
4. XML 外部实体(XXE)
5. 失效的访问控制
6. 安全配置错误
7. 跨站脚本(XSS)
8. 不安全的反序列化
9. 使用含有已知漏洞的组件
10. 不足的日志记录和监控
二、SQL 注入防护
2.1 使用 ORM
# 安全:使用 ORM
Article.objects.filter(title=user_input)
Article.objects.filter(title__contains=user_input)
# 危险:原始 SQL
# 绝对不要这样做!
cursor.execute(f"SELECT * FROM articles WHERE title = '{user_input}'")
# 如果必须使用原始 SQL,使用参数化查询
cursor.execute("SELECT * FROM articles WHERE title = %s", [user_input])
Article.objects.raw("SELECT * FROM articles WHERE title = %s", [user_input])
2.2 extra() 和 RawSQL
from django.db.models import Value
from django.db.models.functions import Concat
# 安全:使用参数
Article.objects.extra(where=["title = %s"], params=[user_input])
# 安全:使用 RawSQL
from django.db.models.expressions import RawSQL
Article.objects.annotate(
custom=RawSQL("title || %s", [user_input])
)
# 不安全:字符串拼接
# 绝对不要这样做!
Article.objects.extra(where=[f"title = '{user_input}'"])
三、XSS 防护
3.1 模板自动转义
<!-- 默认自动转义 -->
{{ user_input }}
<!-- <script>alert('xss')</script> 会被转义 -->
<!-- 禁用转义(仅用于可信内容) -->
{{ trusted_html|safe }} {% autoescape off %} {{ trusted_html }} {% endautoescape
%}
<!-- 强制转义 -->
{{ content|escape }} {{ content|force_escape }}
3.2 JavaScript 中的数据
<!-- 安全:使用 json_script -->
{{ data|json_script:"my-data" }}
<script>
const data = JSON.parse(document.getElementById("my-data").textContent);
</script>
<!-- 不安全:直接插入 -->
<!-- 绝对不要这样做! -->
<script>
const data = {{ data }};
</script>
<!-- 安全:转义 JavaScript -->
<script>
const message = "{{ message|escapejs }}";
</script>
3.3 Content Security Policy
# settings.py
# 使用 django-csp
# pip install django-csp
MIDDLEWARE = [
# ...
'csp.middleware.CSPMiddleware',
]
# CSP 配置
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", 'cdn.example.com')
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", 'data:', 'https:')
CSP_FONT_SRC = ("'self'", 'fonts.gstatic.com')
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_FORM_ACTION = ("'self'",)
# 或使用中间件
class CSPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self' cdn.example.com; "
"style-src 'self' 'unsafe-inline';"
)
return response
四、CSRF 防护
4.1 基本使用
<!-- 表单中使用 -->
<form method="post">
{% csrf_token %}
<input type="text" name="title" />
<button type="submit">提交</button>
</form>
# 视图中强制 CSRF
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def my_view(request):
pass
# 豁免 CSRF(仅用于 API)
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def api_webhook(request):
# 验证其他方式的认证
pass
4.2 AJAX 请求
// 获取 CSRF token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie("csrftoken");
// 发送请求
fetch("/api/endpoint/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify(data),
});
4.3 CSRF 配置
# settings.py
CSRF_COOKIE_SECURE = True # 仅 HTTPS
CSRF_COOKIE_HTTPONLY = False # JavaScript 需要访问
CSRF_COOKIE_SAMESITE = 'Lax' # SameSite 策略
CSRF_COOKIE_AGE = 31449600 # Cookie 有效期
CSRF_TRUSTED_ORIGINS = [ # 受信任的来源
'https://example.com',
'https://www.example.com',
]
五、认证安全
5.1 密码策略
# settings.py
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{
'NAME': 'myapp.validators.ComplexPasswordValidator',
},
]
# 密码哈希算法
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
]
5.2 防暴力破解
# 使用 django-axes
# pip install django-axes
INSTALLED_APPS = [
# ...
'axes',
]
MIDDLEWARE = [
'axes.middleware.AxesMiddleware',
# ...
]
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesStandaloneBackend',
'django.contrib.auth.backends.ModelBackend',
]
# 配置
AXES_FAILURE_LIMIT = 5 # 失败次数限制
AXES_COOLOFF_TIME = 1 # 冷却时间(小时)
AXES_LOCKOUT_TEMPLATE = 'lockout.html'
AXES_RESET_ON_SUCCESS = True
5.3 会话安全
# settings.py
SESSION_COOKIE_SECURE = True # 仅 HTTPS
SESSION_COOKIE_HTTPONLY = True # 禁止 JavaScript 访问
SESSION_COOKIE_SAMESITE = 'Lax' # SameSite 策略
SESSION_COOKIE_AGE = 1209600 # 两周
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 会话引擎
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
# 定期更换会话密钥
def login_view(request):
user = authenticate(...)
if user:
login(request, user)
request.session.cycle_key() # 防止会话固定攻击
六、数据保护
6.1 敏感数据加密
# 使用 django-cryptography
# pip install django-cryptography
from django_cryptography.fields import encrypt
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
ssn = encrypt(models.CharField(max_length=11)) # 加密存储
phone = encrypt(models.CharField(max_length=20))
# 或使用 Fernet 加密
from cryptography.fernet import Fernet
from django.conf import settings
def encrypt_data(data):
f = Fernet(settings.ENCRYPTION_KEY)
return f.encrypt(data.encode()).decode()
def decrypt_data(encrypted_data):
f = Fernet(settings.ENCRYPTION_KEY)
return f.decrypt(encrypted_data.encode()).decode()
6.2 文件上传安全
import magic
from django.core.exceptions import ValidationError
def validate_file_type(file):
"""验证文件类型"""
allowed_types = ['image/jpeg', 'image/png', 'application/pdf']
# 检查文件头而不仅仅是扩展名
mime = magic.from_buffer(file.read(1024), mime=True)
file.seek(0)
if mime not in allowed_types:
raise ValidationError('不支持的文件类型')
def validate_file_size(file):
"""验证文件大小"""
max_size = 10 * 1024 * 1024 # 10MB
if file.size > max_size:
raise ValidationError('文件大小不能超过10MB')
class UploadForm(forms.Form):
file = forms.FileField(
validators=[validate_file_type, validate_file_size]
)
# 安全的文件存储
import uuid
import os
def secure_upload_path(instance, filename):
"""生成安全的上传路径"""
ext = filename.split('.')[-1]
filename = f'{uuid.uuid4()}.{ext}'
return os.path.join('uploads', filename)
class Document(models.Model):
file = models.FileField(upload_to=secure_upload_path)
6.3 日志脱敏
import logging
import re
class SensitiveDataFilter(logging.Filter):
"""敏感数据过滤器"""
patterns = [
(r'password["\']?\s*[:=]\s*["\']?[^"\'&\s]+', 'password=***'),
(r'\b\d{16,19}\b', '****CARD****'), # 信用卡号
(r'\b\d{3}-\d{2}-\d{4}\b', '***-**-****'), # SSN
(r'[\w\.-]+@[\w\.-]+', '***@***.***'), # 邮箱
]
def filter(self, record):
if isinstance(record.msg, str):
for pattern, replacement in self.patterns:
record.msg = re.sub(pattern, replacement, record.msg, flags=re.IGNORECASE)
return True
LOGGING = {
'filters': {
'sensitive_data': {
'()': 'myapp.logging.SensitiveDataFilter',
},
},
'handlers': {
'file': {
'filters': ['sensitive_data'],
# ...
},
},
}
七、API 安全
7.1 认证与授权
# 使用 JWT
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# JWT 配置
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
}
7.2 API 限流
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour',
'login': '5/minute', # 登录限流
},
}
# 自定义限流
from rest_framework.throttling import SimpleRateThrottle
class LoginRateThrottle(SimpleRateThrottle):
scope = 'login'
def get_cache_key(self, request, view):
if request.user.is_authenticated:
return None
return self.cache_format % {
'scope': self.scope,
'ident': self.get_ident(request)
}
7.3 输入验证
from rest_framework import serializers
import bleach
class ArticleSerializer(serializers.ModelSerializer):
content = serializers.CharField()
def validate_content(self, value):
# 清理 HTML
allowed_tags = ['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li']
allowed_attrs = {'a': ['href', 'title']}
return bleach.clean(value, tags=allowed_tags, attributes=allowed_attrs)
def validate_title(self, value):
# 检查长度
if len(value) > 200:
raise serializers.ValidationError('标题过长')
# 检查敏感词
if contains_sensitive_words(value):
raise serializers.ValidationError('标题包含敏感词')
return value
八、安全头
8.1 配置安全头
# settings.py
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# 中间件
class SecurityHeadersMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# 安全头
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response['Permissions-Policy'] = 'geolocation=(), microphone=()'
return response
8.2 HTTPS 配置
# settings.py
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# HSTS
SECURE_HSTS_SECONDS = 31536000 # 1年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
九、安全审计
9.1 安全日志
import logging
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.dispatch import receiver
security_logger = logging.getLogger('security')
@receiver(user_logged_in)
def log_user_login(sender, request, user, **kwargs):
security_logger.info(
f'用户登录成功: {user.username} | IP: {get_client_ip(request)}'
)
@receiver(user_login_failed)
def log_login_failed(sender, credentials, request, **kwargs):
security_logger.warning(
f'登录失败: {credentials.get("username")} | IP: {get_client_ip(request)}'
)
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
9.2 安全扫描
# 使用 bandit 进行代码安全扫描
pip install bandit
bandit -r myproject/
# 使用 safety 检查依赖漏洞
pip install safety
safety check
# Django 安全检查
python manage.py check --deploy
9.3 安全检查清单
# 安全配置检查脚本
def security_check():
from django.conf import settings
issues = []
if settings.DEBUG:
issues.append('DEBUG 应该设置为 False')
if not settings.SECRET_KEY or settings.SECRET_KEY == 'your-secret-key':
issues.append('SECRET_KEY 未正确设置')
if not settings.ALLOWED_HOSTS:
issues.append('ALLOWED_HOSTS 未设置')
if not getattr(settings, 'SECURE_SSL_REDIRECT', False):
issues.append('SECURE_SSL_REDIRECT 应该开启')
if not getattr(settings, 'SESSION_COOKIE_SECURE', False):
issues.append('SESSION_COOKIE_SECURE 应该开启')
if not getattr(settings, 'CSRF_COOKIE_SECURE', False):
issues.append('CSRF_COOKIE_SECURE 应该开启')
return issues
十、安全配置总结
# settings/production.py - 安全配置汇总
# 基础安全
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY')
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# HTTPS
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Cookie
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SAMESITE = 'Lax'
# 安全头
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# 密码
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
]
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12}},
# ...
]