📚 概述
Django的签名系统是一个强大的安全工具,用于创建和验证数字签名,确保数据的完整性和真实性。它基于HMAC-SHA1算法,使用SECRET_KEY进行签名,防止数据被篡改或伪造。
🔐 核心概念
什么是数字签名?
数字签名是一种加密技术,用于:
- 验证数据完整性 - 确保数据没有被篡改
- 验证数据来源 - 确保数据确实来自你的服务器
- 防止伪造 - 防止恶意用户伪造数据
签名格式
1 2 3
| ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk │─────────│──────│─────────────────────────────────│ │ 数据 │时间戳│ HMAC签名 │
|
三部分用冒号分隔:
- 数据部分:原始数据的Base64编码
- 时间戳:Base62编码的时间戳(仅TimestampSigner)
- 签名部分:HMAC-SHA1签名
🛠️ 核心类和函数
1. Signer类
基础签名器,提供基本的签名和验证功能。
1 2 3 4 5 6 7 8 9
| from django.core.signing import Signer
signer = Signer('my-secret-key') signed_value = signer.sign('hello world') print(signed_value)
original_value = signer.unsign(signed_value) print(original_value)
|
2. TimestampSigner类
继承自Signer,添加时间戳功能,支持过期验证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from django.core.signing import TimestampSigner
signer = TimestampSigner('my-secret-key') signed_value = signer.sign('hello world') print(signed_value)
original_value = signer.unsign(signed_value)
try: original_value = signer.unsign(signed_value, max_age=3600) except SignatureExpired: print("签名已过期")
|
3. 便利函数:dumps() 和 loads()
dumps() - 序列化并签名
1 2 3 4 5 6 7 8 9 10 11
| from django.core import signing
token = signing.dumps({'user_id': 123, 'action': 'reset_password'}) print(token)
compressed_token = signing.dumps(large_data, compress=True)
custom_token = signing.dumps(data, salt='my.custom.salt')
|
loads() - 验证并反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| try: data = signing.loads(token) print(data) except signing.BadSignature: print("签名无效")
try: data = signing.loads(token, max_age=3600) except signing.SignatureExpired: print("签名已过期") except signing.BadSignature: print("签名无效")
|
⏰ 时间戳机制详解
时间戳生成
1 2
| def timestamp(self): return baseconv.base62.encode(int(time.time()))
|
时间戳验证
1 2 3 4 5 6 7 8 9 10 11 12
| def unsign(self, value, max_age=None): value, timestamp = result.rsplit(self.sep, 1) timestamp = baseconv.base62.decode(timestamp) if max_age is not None: age = time.time() - timestamp if age > max_age: raise SignatureExpired(...) return value
|
Base62编码
Django使用Base62编码时间戳,包含62个字符:
1
| BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
优势:
- URL安全:不包含特殊字符
- 紧凑性:比十进制更短
- 可读性:混合数字和字母
🎯 实际应用场景
1. CSRF防护
1 2 3 4 5
| def get_token(request): csrf_secret = _get_new_csrf_string() request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret) return _salt_cipher_secret(csrf_secret)
|
2. Session数据(签名Cookie)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def _get_session_key(self): session_cache = getattr(self, '_session_cache', {}) return signing.dumps( session_cache, compress=True, salt='django.contrib.sessions.backends.signed_cookies', )
def load(self): return signing.loads( self.session_key, max_age=settings.SESSION_COOKIE_AGE, salt='django.contrib.sessions.backends.signed_cookies', )
|
3. 密码重置令牌
1 2 3 4 5 6
| context = { 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'token': token_generator.make_token(user), 'protocol': 'https' if use_https else 'http', }
|
4. 邮件激活链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| activation_token = signing.dumps({ 'user_id': user.id, 'email': user.email, 'action': 'activate_account' })
try: data = signing.loads(token, max_age=24*3600) user_id = data['user_id'] except signing.SignatureExpired: return HttpResponse("激活链接已过期")
|
⚙️ 配置设置
默认过期时间设置
1 2 3 4 5 6 7 8 9 10 11 12 13
|
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2
PASSWORD_RESET_TIMEOUT_DAYS = 3
CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52
|
签名后端配置
1 2 3 4 5
| SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
SECRET_KEY = 'your-secret-key-here'
|
🔒 安全特性
1. 基于HMAC-SHA1
1 2
| def base64_hmac(salt, value, key): return b64_encode(salted_hmac(salt, value, key).digest())
|
2. 盐值机制
1 2 3
| def signature(self, value): signature = base64_hmac(self.salt + 'signer', value, self.key) return force_str(signature)
|
3. 常量时间比较
1 2 3 4 5
| def unsign(self, signed_value): if constant_time_compare(sig, self.signature(value)): return force_text(value) raise BadSignature(...)
|
📋 异常处理
异常类型
1 2 3 4 5 6 7
| class BadSignature(Exception): """签名不匹配""" pass
class SignatureExpired(BadSignature): """签名时间戳超过required max_age""" pass
|
异常处理示例
1 2 3 4 5 6 7 8 9 10
| from django.core import signing
def verify_token(token): try: data = signing.loads(token, max_age=3600) return data, None except signing.SignatureExpired: return None, "令牌已过期,请重新获取" except signing.BadSignature: return None, "令牌无效,可能被篡改"
|
💡 最佳实践
1. 根据场景设置合适的有效期
1 2 3 4 5 6 7 8 9 10 11
| password_reset_token = signing.dumps(user_data) signing.loads(password_reset_token, max_age=3600)
remember_token = signing.dumps(user_data) signing.loads(remember_token, max_age=30*24*3600)
temp_token = signing.dumps(operation_data) signing.loads(temp_token, max_age=300)
|
2. 使用不同的salt
1 2 3 4
| email_token = signing.dumps(data, salt='email.verification') download_token = signing.dumps(data, salt='file.download') api_token = signing.dumps(data, salt='api.access')
|
3. 处理异常
1 2 3 4 5 6 7
| def safe_loads(token, max_age=None): try: return signing.loads(token, max_age=max_age), None except signing.SignatureExpired: return None, "TOKEN_EXPIRED" except signing.BadSignature: return None, "TOKEN_INVALID"
|
4. 生产环境考虑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
max_age = 3600 + 60
import logging logger = logging.getLogger(__name__)
try: data = signing.loads(token, max_age=3600) except signing.SignatureExpired: logger.warning(f"Token expired for user {request.user.id}") raise
|
🔄 完整工作流程
签名生成流程
- 数据序列化:将Python对象转换为JSON
- 数据压缩:可选,如果启用compress=True
- Base64编码:将数据转换为URL安全格式
- 添加时间戳:TimestampSigner添加当前时间戳
- 生成签名:使用HMAC-SHA1和SECRET_KEY生成签名
- 组合结果:将数据、时间戳、签名用冒号连接
签名验证流程
- 分离组件:按冒号分离数据、时间戳、签名
- 验证签名:重新计算签名并与提供的签名比较
- 检查时间:如果指定max_age,检查是否过期
- 解码数据:Base64解码和JSON反序列化
- 返回结果:返回原始数据或抛出异常
🚀 示例:构建邮件验证系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from django.core import signing from django.shortcuts import render, redirect from django.http import HttpResponse import datetime
def send_verification_email(request): """发送验证邮件""" user_email = request.POST.get('email') token_data = { 'email': user_email, 'action': 'verify_email', 'user_id': request.user.id } verification_token = signing.dumps(token_data, salt='email.verification') verification_url = f"https://example.com/verify/{verification_token}/" return HttpResponse("验证邮件已发送")
def verify_email(request, token): """验证邮件""" try: data = signing.loads( token, salt='email.verification', max_age=24*3600 ) email = data['email'] user_id = data['user_id'] action = data['action'] if action != 'verify_email': return HttpResponse("无效的验证类型", status=400) return HttpResponse("邮箱验证成功!") except signing.SignatureExpired: return HttpResponse("验证链接已过期,请重新发送验证邮件", status=400) except signing.BadSignature: return HttpResponse("无效的验证链接", status=400)
|
📊 性能考虑
时间复杂度
- 签名生成:O(n),n为数据大小
- 签名验证:O(n),n为数据大小
- 时间戳编码/解码:O(1)
空间效率
- Base62编码:比Base10更紧凑
- 压缩选项:大数据可以启用compress=True
- 签名开销:固定大小的签名部分
缓存策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from django.core.cache import cache
def cached_verify_token(token): cache_key = f"token_verify_{hash(token)}" result = cache.get(cache_key) if result is None: try: result = signing.loads(token, max_age=3600) cache.set(cache_key, result, timeout=300) except signing.BadSignature: result = False cache.set(cache_key, result, timeout=60) return result
|
🔍 调试技巧
查看签名组成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def debug_signature(signed_value): """调试签名组成部分""" parts = signed_value.split(':') print(f"总共 {len(parts)} 部分:") if len(parts) >= 2: print(f"数据部分: {parts[0]}") print(f"最后部分(签名): {parts[-1]}") if len(parts) == 3: print(f"时间戳部分: {parts[1]}") from django.utils import baseconv timestamp = baseconv.base62.decode(parts[1]) import datetime dt = datetime.datetime.fromtimestamp(timestamp) print(f"生成时间: {dt}")
token = signing.dumps("hello world") debug_signature(token)
|
测试不同场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import time from django.core import signing
def test_signing_scenarios(): """测试各种签名场景""" signer = signing.Signer() simple_signed = signer.sign("test data") print("基本签名:", simple_signed) timestamp_signer = signing.TimestampSigner() time_signed = timestamp_signer.sign("test data") print("时间戳签名:", time_signed) complex_data = { 'user_id': 123, 'permissions': ['read', 'write'], 'expires': '2024-12-31' } complex_signed = signing.dumps(complex_data) print("复杂数据签名:", complex_signed) large_data = list(range(100)) compressed = signing.dumps(large_data, compress=True) uncompressed = signing.dumps(large_data, compress=False) print(f"压缩后长度: {len(compressed)}") print(f"未压缩长度: {len(uncompressed)}") print(f"压缩率: {len(compressed)/len(uncompressed)*100:.1f}%")
test_signing_scenarios()
|
🎯 总结
Django的签名系统是一个精心设计的安全工具,具有以下特点:
✅ 优势
- 安全可靠:基于HMAC-SHA1,使用SECRET_KEY
- 使用简单:提供便利的dumps/loads接口
- 功能完整:支持时间戳、压缩、自定义salt
- 性能良好:高效的编码和签名算法
- 应用广泛:CSRF、Session、密码重置等场景
⚠️ 注意事项
- SECRET_KEY安全:必须保证SECRET_KEY的安全性
- 时间同步:服务器时间需要准确同步
- 过期处理:合理设置max_age参数
- 异常处理:正确处理签名异常
- 性能监控:大量签名操作需要性能监控
🔮 适用场景
- ✅ 无状态令牌验证
- ✅ 临时链接生成
- ✅ 客户端数据存储
- ✅ API认证令牌
- ❌ 长期存储加密
- ❌ 敏感数据加密
Django签名系统为现代Web应用提供了强大的安全基础,是构建安全可靠应用的重要工具。