django安全最佳实践 - 防护常见Web安全漏洞
📂 所属阶段:第三部分 — 高级主题
🎯 难度等级:高级
⏰ 预计学习时间:3-4小时
🎒 前置知识:用户认证系统
目录
安全概述
Web安全是一道不能触碰的红线。好在django提供了强大的内置防护,但前提是——我们必须正确配置和使用。本节将带你快速理解最需要关注的几类攻击,以及django是怎么应对的。
和django最相关的OWASP Top 5
- 注入攻击:django ORM防止SQL注入,模板自动转义防止XSS
- 失效的访问控制:通过装饰器和权限系统来控制访问
- 身份验证失败:内建的认证中间件和密码验证器保驾护航
- 安全配置错误:内置
check --deploy 一键发现配置问题
- CSRF:原生的CSRF中间件直接防御跨站请求伪造
核心安全原则
- 最小权限:只给用户和进程最必要的权限,避免随意使用超级用户
- 输入白名单,输出全转义:不要用模糊过滤代替白名单,所有输出到页面的数据必须转义
- 深度防御:层层设防,就算一层被突破,后面还有保护
- 不暴露敏感信息:错误页面、日志中禁止泄露密钥、路径等敏感数据
XSS防护
跨站脚本(XSS)是最常见的Web攻击之一。简单说,就是攻击者在页面里注入恶意脚本,当其他用户访问时脚本被执行,从而窃取信息或冒充身份。
1. 模板自动转义——第一道防线
django模板引擎默认会转义所有HTML特殊字符,把 < 变成 <,这样脚本就不会被执行。我们唯一要做的就是不要随便禁用这个保护。
错误用法
随意使用 |safe 过滤器就是在开门揖盗:
<!-- 危险!不要这样 -->
<div>{{ user_comment|safe }}</div>
正确用法
保持自动转义,或者明确使用 escape 过滤器,让输出安全落地:
<!-- 安全:自动转义 -->
<div>{{ user_comment }}</div>
<!-- 显式转义(效果相同) -->
<div>{{ user_comment|escape }}</div>
2. 富文本怎么处理?用 bleach
有些场景确实需要接收用户输入的富文本,比如评论可以包含粗体、换行等。这时候不能简单放开 safe,而应该用专门的清理库,比如 bleach。
安装 bleach:
创建自定义模板过滤器,只允许安全的标签和属性:
# myapp/templatetags/safe_tags.py
import bleach
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter
def safe_html(text):
allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'code', 'pre']
allowed_attrs = {'code': ['class']}
cleaned = bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, styles=[], strip=True)
return mark_safe(cleaned)
模板中使用时,加载过滤器并调用即可:
{% load safe_tags %}
<div>{{ user_comment|safe_html }}</div>
3. 内容安全策略(CSP)
CSP是一道由浏览器执行的额外防线,通过HTTP响应头告诉浏览器:哪些来源的资源可以加载,哪些脚本可以执行。django内置了部分支持。
最基础的配置就是启用 SecurityMiddleware 并开启几个安全头:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # django内置,一键配置部分安全头
# 其他中间件...
]
# 一键启用部分安全头
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
更完整的CSP策略,可以通过自定义中间件或者在反向代理(如Nginx)层配置,这样可以精细控制每一个资源类型。
CSRF防护
跨站请求伪造(CSRF)利用的是用户已登录的身份,在用户不知情的情况下,诱导浏览器发出恶意请求。django的原生CSRF中间件可以防住绝大多数情况。
1. 中间件不要动
确保 django.middleware.csrf.CsrfViewMiddleware 在 MIDDLEWARE 中,而且顺序正确。它默认就是开启的,千万不要去掉。
2. 表单里带上CSRF令牌
所有通过 POST 请求提交的表单,都要在里面加上 {% csrf_token %}:
<form method="post">
{% csrf_token %}
<input type="text" name="comment">
<button type="submit">提交</button>
</form>
3. AJAX请求也别忘了令牌
前端用 Axios 时,可以自动带上CSRF令牌。先从Cookie里取,然后设置到请求头里:
// 从Cookie获取csrftoken
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;
}
// 配置Axios
import axios from 'axios';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
4. 关键 settings 加固
生产环境一定要收紧配置:
# settings.py
CSRF_COOKIE_SECURE = True # 仅通过HTTPS传输cookie
CSRF_COOKIE_SAMESITE = 'Strict' # 严格限制跨站请求携带cookie
CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com'] # 信任的来源
SQL注入防护
SQL注入通过拼接恶意SQL语句来操控数据库,可能导致数据泄露甚至服务器被控制。django ORM 是防 SQL 注入的基石。
1. 绝对不要拼接 SQL
下面这种操作就是在亲手制造漏洞:
::: danger 绝对不要这样
# 危险!SQL拼接
username = request.GET.get('username')
user = User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")
:::
2. 正确使用 ORM 和参数化
所有查询都应该通过 ORM 或者参数化的原生 SQL 来进行:
::: tip 安全用法
# 安全:ORM 自动参数化
username = request.GET.get('username')
user = User.objects.filter(username=username).first()
# 原生 SQL 也必须参数化
user = User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])
# cursor同样需要参数化
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM auth_user WHERE username = %s", [username])
user = cursor.fetchone()
:::
这些写法能确保用户输入被当作数据,而不是被解释为SQL代码,从根源上杜绝注入。
其他核心安全防护
1. 点击劫持防护
点击劫持攻击会把你的页面藏在一个透明的 iframe 里,诱导用户点击。django 内置了 XFrameOptionsMiddleware,默认就设置了合适的响应头。
可以通过配置来调整策略:
# settings.py(可选调整)
X_FRAME_OPTIONS = 'DENY' # 完全禁止(生产环境推荐)
# X_FRAME_OPTIONS = 'SAMEORIGIN' # 仅允许同源页面内嵌
2. 文件上传安全
文件上传是高危操作,想让上传安全,必须从三个维度验证:
- 扩展名白名单
- MIME类型检查
- 文件内容完整性校验(特别是图片)
下面是一个用 django 表单实现的例子,使用了 python-magic 和 Pillow:
# myapp/forms.py
from django import forms
from PIL import Image
import magic
class SafeFileUploadForm(forms.Form):
file = forms.FileField()
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif'}
ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
def clean_file(self):
file = self.cleaned_data['file']
# 1. 验证扩展名
ext = '.' + file.name.split('.')[-1].lower()
if ext not in self.ALLOWED_EXTENSIONS:
raise forms.ValidationError('不允许的文件类型')
# 2. 验证MIME类型(用python-magic)
file.seek(0)
mime = magic.Magic(mime=True)
mime_type = mime.from_buffer(file.read(1024))
file.seek(0)
if mime_type not in self.ALLOWED_MIME_TYPES:
raise forms.ValidationError('文件类型不匹配')
# 3. 验证图片完整性
if mime_type.startswith('image/'):
try:
img = Image.open(file)
img.verify()
file.seek(0)
except Exception:
raise forms.ValidationError('图片文件损坏或不安全')
return file
3. 密码安全
密码存储和验证绝不能马虎。django 提供了完善的密码验证器,生产环境务必启用并提高强度:
# 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'},
]
生产环境安全配置
1. 敏感信息用环境变量管理
敏感数据(密钥、数据库密码)绝对不能硬编码在代码里。推荐使用 python-decouple 从 .env 文件读取:
先安装:
pip install python-decouple
创建 .env 文件(记得加到 .gitignore):
SECRET_KEY=your-production-secret-key-never-expose
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgres://user:pass@localhost/db
在 settings.py 里加载:
from decouple import config, Csv
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
2. 一键部署检查
django 内置了一个检查命令,能发现很多常见的部署安全问题:
python manage.py check --deploy
运行后根据提示修正所有问题,尤其是 SECRET_KEY 未设置或 DEBUG 依然开启这类高风险项。
安全测试与工具
1. 编写基础安全测试用例
把安全要求写成自动化测试,每次改动都能自动验证:
# myapp/tests/test_security.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
class SecurityTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test', 'test@test.com', 'StrongPass123!')
def test_csrf_protection(self):
# 无CSRF令牌的POST应该被拒绝
response = self.client.post(reverse('submit_comment'), {'text': 'test'})
self.assertEqual(response.status_code, 403)
def test_xss_protection(self):
# 输入脚本应该被转义,原本的脚本字符串不会出现在响应页面里
response = self.client.get(f"{reverse('search')}?q=<script>alert(1)</script>")
self.assertNotContains(response, '<script>alert(1)</script>')
self.assertContains(response, '<script>alert(1)</script>')
2. 常用安全扫描工具
定期跑一下这些工具,能帮你及时堵上已知的漏洞。
本章小结
- django 已经帮你挡掉了大部分 Web 攻击,但你必须正确配置和使用
- 核心理念就两条:输入用白名单,输出全转义
- 永远不要拼接 SQL,坚持用 ORM 或参数化查询
- 生产环境必须关闭
DEBUG,强制使用 HTTPS,并开启各项安全头
- 善用自动化工具:
check --deploy、Bandit、Safety,定期扫描
安全不是一次性的配置,而是一个持续的工程习惯。
下一步学习
🔗 相关教程推荐
🏷️ 核心标签:django安全 XSS防护 CSRF防护 SQL注入 生产环境配置