173 lines
5.5 KiB
Python
173 lines
5.5 KiB
Python
# ===========================================
|
||
# 班级操行分管理系统 - 后端服务
|
||
#
|
||
# 开发者: Canglan
|
||
# 联系方式: admin@sea-studio.top
|
||
# 版权归属: Sea Network Technology Studio
|
||
# 许可证: MIT License
|
||
#
|
||
# 版权所有 © Sea Network Technology Studio
|
||
# ===========================================
|
||
|
||
import hashlib
|
||
import secrets
|
||
import re
|
||
from passlib.hash import bcrypt as bcrypt_hash
|
||
from config import settings
|
||
|
||
|
||
class SecurityUtils:
|
||
"""安全工具类"""
|
||
|
||
@staticmethod
|
||
def sha1_md5_password(password: str) -> str:
|
||
"""
|
||
双重加密:sha1 + md5
|
||
流程:原始密码 -> sha1 -> 加盐 -> md5
|
||
"""
|
||
# 第一层:SHA1
|
||
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest()
|
||
# 加盐
|
||
salted = sha1_hash + settings.PASSWORD_SALT
|
||
# 第二层:MD5
|
||
md5_hash = hashlib.md5(salted.encode('utf-8')).hexdigest()
|
||
return md5_hash
|
||
|
||
@staticmethod
|
||
def bcrypt_password(password: str) -> str:
|
||
"""使用bcrypt加密密码"""
|
||
return bcrypt_hash.using(rounds=12).hash(password)
|
||
|
||
@staticmethod
|
||
def verify_password_v2(plain_password: str, hashed_password: str) -> tuple:
|
||
"""
|
||
验证密码(支持bcrypt和旧哈希)
|
||
返回: (是否验证成功, 是否需要升级哈希)
|
||
"""
|
||
try:
|
||
if bcrypt_hash.verify(plain_password, hashed_password):
|
||
return True, False
|
||
except Exception:
|
||
pass
|
||
# 回退到旧的sha1_md5验证
|
||
if SecurityUtils.sha1_md5_password(plain_password) == hashed_password:
|
||
return True, True
|
||
return False, False
|
||
|
||
@staticmethod
|
||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||
"""验证密码"""
|
||
return SecurityUtils.sha1_md5_password(plain_password) == hashed_password
|
||
|
||
@staticmethod
|
||
def generate_random_password(length: int = 8) -> str:
|
||
"""生成随机密码"""
|
||
alphabet = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||
|
||
@staticmethod
|
||
def validate_password_strength(password: str) -> tuple:
|
||
"""
|
||
验证密码强度
|
||
要求:大小写字母、数字、特殊符号 至少包含其中3种
|
||
返回: (是否有效, 错误信息)
|
||
"""
|
||
if len(password) < 6:
|
||
return False, "密码长度至少6位"
|
||
if len(password) > 20:
|
||
return False, "密码长度不能超过20位"
|
||
|
||
# 检查四种字符类型
|
||
has_upper = any(c.isupper() for c in password) # 大写字母
|
||
has_lower = any(c.islower() for c in password) # 小写字母
|
||
has_digit = any(c.isdigit() for c in password) # 数字
|
||
has_special = any(not c.isalnum() for c in password) # 特殊符号
|
||
|
||
# 统计满足的字符类型数量
|
||
char_types = sum([has_upper, has_lower, has_digit, has_special])
|
||
|
||
if char_types < 3:
|
||
return False, "密码必须包含大写字母、小写字母、数字、特殊符号中的至少3种"
|
||
|
||
return True, ""
|
||
|
||
@staticmethod
|
||
def sanitize_string(value: str, max_length: int = 255) -> str:
|
||
"""
|
||
清理字符串输入
|
||
- 去除首尾空格
|
||
- 限制长度
|
||
- 转义特殊字符
|
||
"""
|
||
if not value:
|
||
return ""
|
||
|
||
# 去除首尾空格
|
||
value = value.strip()
|
||
|
||
# SQL注入模式检测
|
||
sql_patterns = [
|
||
r'(?i)(\bunion\b\s+\bselect\b)',
|
||
r'(?i)(\bor\b\s+\d+\s*=\s*\d+)',
|
||
r'(?i)(\bdrop\b\s+\btable\b)',
|
||
r'(?i)(\bdelete\b\s+\bfrom\b)',
|
||
r'(?i)(\binsert\b\s+\binto\b)',
|
||
r'(?i)(\bupdate\b\s+\w+\s+\bset\b)',
|
||
]
|
||
for pattern in sql_patterns:
|
||
value = re.sub(pattern, '', value)
|
||
|
||
# 路径遍历检测
|
||
value = value.replace('../', '').replace('..\\', '')
|
||
|
||
# 限制长度
|
||
if len(value) > max_length:
|
||
value = value[:max_length]
|
||
|
||
# 转义HTML特殊字符(防止XSS)
|
||
html_chars = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
'/': '/'
|
||
}
|
||
for char, escape in html_chars.items():
|
||
value = value.replace(char, escape)
|
||
|
||
return value
|
||
|
||
@staticmethod
|
||
def validate_student_no(student_no: str) -> bool:
|
||
"""验证学号格式(数字+字母,长度4-20)"""
|
||
if not student_no:
|
||
return False
|
||
if len(student_no) < 4 or len(student_no) > 20:
|
||
return False
|
||
# 字母数字组合
|
||
return student_no.isalnum()
|
||
|
||
@staticmethod
|
||
def validate_phone(phone: str) -> bool:
|
||
"""验证手机号格式(中国手机号)"""
|
||
if not phone:
|
||
return False
|
||
pattern = r'^1[3-9]\d{9}$'
|
||
return bool(re.match(pattern, phone))
|
||
|
||
@staticmethod
|
||
def validate_points_change(points: int, max_abs: int = 100) -> tuple:
|
||
"""
|
||
验证分值变动
|
||
返回: (是否有效, 错误信息)
|
||
"""
|
||
if points == 0:
|
||
return False, "分值不能为0"
|
||
if abs(points) > max_abs:
|
||
return False, f"单次分值变动不能超过{max_abs}分"
|
||
return True, ""
|
||
|
||
|
||
# 单例导出
|
||
security = SecurityUtils() |