前后端第一版提交
This commit is contained in:
5
hertz_studio_django_utils/validators/__init__.py
Normal file
5
hertz_studio_django_utils/validators/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .email_validator import EmailValidator
|
||||
from .phone_validator import PhoneValidator
|
||||
from .password_validator import PasswordValidator
|
||||
|
||||
__all__ = ['EmailValidator', 'PhoneValidator', 'PasswordValidator']
|
||||
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class EmailValidator:
|
||||
"""
|
||||
邮箱验证器
|
||||
提供邮箱格式验证功能
|
||||
"""
|
||||
|
||||
# 邮箱正则表达式
|
||||
EMAIL_PATTERN = re.compile(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""
|
||||
验证邮箱格式是否正确
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
bool: 邮箱格式是否正确
|
||||
"""
|
||||
if not email or not isinstance(email, str):
|
||||
return False
|
||||
|
||||
return bool(EmailValidator.EMAIL_PATTERN.match(email.strip()))
|
||||
|
||||
@staticmethod
|
||||
def validate_email(email: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证邮箱并返回详细信息
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否有效, 提示信息)
|
||||
"""
|
||||
if not email:
|
||||
return False, "邮箱地址不能为空"
|
||||
|
||||
if not isinstance(email, str):
|
||||
return False, "邮箱地址必须是字符串"
|
||||
|
||||
email = email.strip()
|
||||
|
||||
if len(email) == 0:
|
||||
return False, "邮箱地址不能为空"
|
||||
|
||||
if len(email) > 254:
|
||||
return False, "邮箱地址长度不能超过254个字符"
|
||||
|
||||
if not EmailValidator.EMAIL_PATTERN.match(email):
|
||||
return False, "邮箱地址格式不正确"
|
||||
|
||||
# 检查本地部分长度(@符号前的部分)
|
||||
local_part = email.split('@')[0]
|
||||
if len(local_part) > 64:
|
||||
return False, "邮箱用户名部分长度不能超过64个字符"
|
||||
|
||||
return True, "邮箱地址格式正确"
|
||||
|
||||
@staticmethod
|
||||
def normalize_email(email: str) -> str:
|
||||
"""
|
||||
标准化邮箱地址
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
str: 标准化后的邮箱地址
|
||||
"""
|
||||
if not email or not isinstance(email, str):
|
||||
return ''
|
||||
|
||||
# 去除首尾空格并转换为小写
|
||||
return email.strip().lower()
|
||||
|
||||
@staticmethod
|
||||
def get_email_domain(email: str) -> str:
|
||||
"""
|
||||
获取邮箱域名
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
str: 邮箱域名
|
||||
"""
|
||||
if not EmailValidator.is_valid_email(email):
|
||||
return ''
|
||||
|
||||
return email.split('@')[1].lower()
|
||||
|
||||
@staticmethod
|
||||
def is_common_email_provider(email: str) -> bool:
|
||||
"""
|
||||
检查是否为常见邮箱服务商
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
bool: 是否为常见邮箱服务商
|
||||
"""
|
||||
common_providers = {
|
||||
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
|
||||
'163.com', '126.com', 'qq.com', 'sina.com', 'sohu.com'
|
||||
}
|
||||
|
||||
domain = EmailValidator.get_email_domain(email)
|
||||
return domain in common_providers
|
||||
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import re
|
||||
from typing import Tuple, List
|
||||
|
||||
|
||||
class PasswordValidator:
|
||||
"""
|
||||
密码验证器
|
||||
提供密码强度验证功能
|
||||
"""
|
||||
|
||||
"""
|
||||
生产环境
|
||||
"""
|
||||
# @staticmethod
|
||||
# def validate_password_strength(password: str, min_length: int = 8, max_length: int = 128) -> Tuple[bool, List[str]]:
|
||||
# """
|
||||
# 验证密码强度
|
||||
#
|
||||
# Args:
|
||||
# password: 密码
|
||||
# min_length: 最小长度
|
||||
# max_length: 最大长度
|
||||
#
|
||||
# Returns:
|
||||
# Tuple[bool, List[str]]: (是否通过验证, 错误信息列表)
|
||||
# """
|
||||
# errors = []
|
||||
#
|
||||
# if not password:
|
||||
# errors.append("密码不能为空")
|
||||
# return False, errors
|
||||
#
|
||||
# if not isinstance(password, str):
|
||||
# errors.append("密码必须是字符串")
|
||||
# return False, errors
|
||||
#
|
||||
# # 检查长度
|
||||
# if len(password) < min_length:
|
||||
# errors.append(f"密码长度至少{min_length}位")
|
||||
#
|
||||
# if len(password) > max_length:
|
||||
# errors.append(f"密码长度不能超过{max_length}位")
|
||||
#
|
||||
# # 检查是否包含数字
|
||||
# if not re.search(r'\d', password):
|
||||
# errors.append("密码必须包含至少一个数字")
|
||||
#
|
||||
# # 检查是否包含小写字母
|
||||
# if not re.search(r'[a-z]', password):
|
||||
# errors.append("密码必须包含至少一个小写字母")
|
||||
#
|
||||
# # 检查是否包含大写字母
|
||||
# if not re.search(r'[A-Z]', password):
|
||||
# errors.append("密码必须包含至少一个大写字母")
|
||||
#
|
||||
# return len(errors) == 0, errors
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
开发环境
|
||||
"""
|
||||
# 默认使用下面开发环境,在生产环境中请使用上面的校验规则!
|
||||
@staticmethod
|
||||
def validate_password_strength(password: str,
|
||||
min_length: int = 6,
|
||||
max_length: int = 128) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
验证密码强度(仅检查长度,不少于 6 位即可)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
min_length: 最小长度(默认 6)
|
||||
max_length: 最大长度(默认 128)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, List[str]]: (是否通过验证, 错误信息列表)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if not password:
|
||||
errors.append("密码不能为空")
|
||||
return False, errors
|
||||
|
||||
if not isinstance(password, str):
|
||||
errors.append("密码必须是字符串")
|
||||
return False, errors
|
||||
|
||||
# 仅长度检查
|
||||
if len(password) < min_length:
|
||||
errors.append(f"密码长度至少{min_length}位")
|
||||
|
||||
if len(password) > max_length:
|
||||
errors.append(f"密码长度不能超过{max_length}位")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
@staticmethod
|
||||
def validate_simple_password(password: str, min_length: int = 6, max_length: int = 128) -> Tuple[bool, str]:
|
||||
"""
|
||||
简单密码验证(只检查长度和基本字符)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
min_length: 最小长度
|
||||
max_length: 最大长度
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否通过验证, 错误信息)
|
||||
"""
|
||||
if not password:
|
||||
return False, "密码不能为空"
|
||||
|
||||
if not isinstance(password, str):
|
||||
return False, "密码必须是字符串"
|
||||
|
||||
if len(password) < min_length:
|
||||
return False, f"密码长度至少{min_length}位"
|
||||
|
||||
if len(password) > max_length:
|
||||
return False, f"密码长度不能超过{max_length}位"
|
||||
|
||||
# 检查是否包含数字或字母
|
||||
if not re.search(r'[a-zA-Z0-9]', password):
|
||||
return False, "密码必须包含字母或数字"
|
||||
|
||||
return True, "密码格式正确"
|
||||
|
||||
@staticmethod
|
||||
def check_common_passwords(password: str) -> bool:
|
||||
"""
|
||||
检查是否为常见弱密码
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
bool: 是否为常见弱密码
|
||||
"""
|
||||
common_passwords = {
|
||||
'123456', 'password', '123456789', '12345678', '12345',
|
||||
'1234567', '1234567890', 'qwerty', 'abc123', '111111',
|
||||
'123123123', 'admin', 'letmein', 'welcome', 'monkey',
|
||||
'1234', 'dragon', 'pass', 'master', 'hello',
|
||||
'freedom', 'whatever', 'qazwsx', 'trustno1', 'jordan23'
|
||||
}
|
||||
|
||||
return password.lower() in common_passwords
|
||||
|
||||
@staticmethod
|
||||
def calculate_password_score(password: str) -> int:
|
||||
"""
|
||||
计算密码强度分数(0-100)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
int: 密码强度分数
|
||||
"""
|
||||
if not password:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
|
||||
# 长度分数(最多30分)
|
||||
length_score = min(len(password) * 2, 30)
|
||||
score += length_score
|
||||
|
||||
# 字符类型分数
|
||||
if re.search(r'[a-z]', password): # 小写字母
|
||||
score += 10
|
||||
|
||||
if re.search(r'[A-Z]', password): # 大写字母
|
||||
score += 10
|
||||
|
||||
if re.search(r'\d', password): # 数字
|
||||
score += 10
|
||||
|
||||
if re.search(r'[!@#$%^&*(),.?":{}|<>]', password): # 特殊字符
|
||||
score += 15
|
||||
|
||||
# 字符多样性分数
|
||||
unique_chars = len(set(password))
|
||||
diversity_score = min(unique_chars * 2, 25)
|
||||
score += diversity_score
|
||||
|
||||
# 扣分项
|
||||
if PasswordValidator.check_common_passwords(password):
|
||||
score -= 30
|
||||
|
||||
# 重复字符扣分
|
||||
if re.search(r'(.)\1{2,}', password): # 连续3个相同字符
|
||||
score -= 10
|
||||
|
||||
# 连续数字或字母扣分
|
||||
if re.search(r'(012|123|234|345|456|567|678|789|890)', password):
|
||||
score -= 5
|
||||
|
||||
if re.search(r'(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)', password.lower()):
|
||||
score -= 5
|
||||
|
||||
return max(0, min(100, score))
|
||||
|
||||
@staticmethod
|
||||
def get_password_strength_level(password: str) -> str:
|
||||
"""
|
||||
获取密码强度等级
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
str: 密码强度等级
|
||||
"""
|
||||
score = PasswordValidator.calculate_password_score(password)
|
||||
|
||||
if score >= 80:
|
||||
return "很强"
|
||||
elif score >= 60:
|
||||
return "强"
|
||||
elif score >= 40:
|
||||
return "中等"
|
||||
elif score >= 20:
|
||||
return "弱"
|
||||
else:
|
||||
return "很弱"
|
||||
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class PhoneValidator:
|
||||
"""
|
||||
手机号验证器
|
||||
提供手机号格式验证功能
|
||||
"""
|
||||
|
||||
# 中国大陆手机号正则表达式
|
||||
CHINA_MOBILE_PATTERN = re.compile(
|
||||
r'^1[3-9]\d{9}$'
|
||||
)
|
||||
|
||||
# 国际手机号正则表达式(简化版)
|
||||
INTERNATIONAL_PATTERN = re.compile(
|
||||
r'^\+?[1-9]\d{1,14}$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_valid_china_mobile(phone: str) -> bool:
|
||||
"""
|
||||
验证中国大陆手机号格式是否正确
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return False
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
return bool(PhoneValidator.CHINA_MOBILE_PATTERN.match(phone))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_international_phone(phone: str) -> bool:
|
||||
"""
|
||||
验证国际手机号格式是否正确
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return False
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
return bool(PhoneValidator.INTERNATIONAL_PATTERN.match(phone))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_phone(phone: str) -> bool:
|
||||
"""
|
||||
验证手机号格式是否正确(默认使用中国大陆手机号验证)
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
return PhoneValidator.is_valid_china_mobile(phone)
|
||||
|
||||
@staticmethod
|
||||
def validate_china_mobile(phone: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证中国大陆手机号并返回详细信息
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否有效, 提示信息)
|
||||
"""
|
||||
if not phone:
|
||||
return False, "手机号不能为空"
|
||||
|
||||
if not isinstance(phone, str):
|
||||
return False, "手机号必须是字符串"
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
if len(phone) == 0:
|
||||
return False, "手机号不能为空"
|
||||
|
||||
if len(phone) != 11:
|
||||
return False, "手机号长度必须为11位"
|
||||
|
||||
if not phone.isdigit():
|
||||
return False, "手机号只能包含数字"
|
||||
|
||||
if not phone.startswith('1'):
|
||||
return False, "手机号必须以1开头"
|
||||
|
||||
if phone[1] not in '3456789':
|
||||
return False, "手机号第二位必须是3-9之间的数字"
|
||||
|
||||
return True, "手机号格式正确"
|
||||
|
||||
@staticmethod
|
||||
def normalize_phone(phone: str) -> str:
|
||||
"""
|
||||
标准化手机号
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
str: 标准化后的手机号
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return ''
|
||||
|
||||
# 去除空格、连字符和括号
|
||||
phone = phone.replace(' ', '').replace('-', '').replace('(', '').replace(')', '')
|
||||
|
||||
# 如果是中国大陆手机号且以+86开头,去除+86
|
||||
if phone.startswith('+86') and len(phone) == 14:
|
||||
phone = phone[3:]
|
||||
elif phone.startswith('86') and len(phone) == 13:
|
||||
phone = phone[2:]
|
||||
|
||||
return phone
|
||||
|
||||
@staticmethod
|
||||
def get_mobile_carrier(phone: str) -> str:
|
||||
"""
|
||||
获取手机号运营商(仅支持中国大陆)
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
str: 运营商名称
|
||||
"""
|
||||
if not PhoneValidator.is_valid_china_mobile(phone):
|
||||
return '未知'
|
||||
|
||||
phone = PhoneValidator.normalize_phone(phone)
|
||||
prefix = phone[:3]
|
||||
|
||||
# 中国移动
|
||||
china_mobile_prefixes = {
|
||||
'134', '135', '136', '137', '138', '139',
|
||||
'147', '150', '151', '152', '157', '158', '159',
|
||||
'172', '178', '182', '183', '184', '187', '188',
|
||||
'195', '197', '198'
|
||||
}
|
||||
|
||||
# 中国联通
|
||||
china_unicom_prefixes = {
|
||||
'130', '131', '132', '145', '155', '156',
|
||||
'166', '171', '175', '176', '185', '186', '196'
|
||||
}
|
||||
|
||||
# 中国电信
|
||||
china_telecom_prefixes = {
|
||||
'133', '149', '153', '173', '174', '177',
|
||||
'180', '181', '189', '191', '193', '199'
|
||||
}
|
||||
|
||||
if prefix in china_mobile_prefixes:
|
||||
return '中国移动'
|
||||
elif prefix in china_unicom_prefixes:
|
||||
return '中国联通'
|
||||
elif prefix in china_telecom_prefixes:
|
||||
return '中国电信'
|
||||
else:
|
||||
return '未知运营商'
|
||||
Reference in New Issue
Block a user