import _thread import logging from datetime import timedelta from django.db import models, transaction from django.utils.crypto import get_random_string from django.core.mail import send_mail from django.utils import timezone from django.template.loader import render_to_string from django.conf import settings from sms import send_sms_message from pingcap_sso.exceptions import RateLimitError logger = logging.getLogger(__name__) def generate_code(): return get_random_string(length=6, allowed_chars='0123456789') class SecurityCode(models.Model): SCENE_SIGNUP = 'signup' SCENE_LOGIN = 'login' SCENE_PARTNER_CERTIFICATION = 'partner_certification' SCENE_OFFICIAL_CONTACT_US = 'official_contact_us' SCENE_UPDATE_PHONE = 'update_phone' DELIVER_BY_PHONE = 0 DELIVER_BY_EMAIL = 1 DELIVER_CHANNLE_CHOICES = ( (DELIVER_BY_PHONE, 'phone'), (DELIVER_BY_EMAIL, 'email') ) code = models.CharField('code', max_length=8, default=generate_code) deliver_address = models.CharField( 'deliver address', max_length=128, help_text='Can be email or phone') deliver_channel = models.SmallIntegerField( 'deliver channel', choices=DELIVER_CHANNLE_CHOICES, default=DELIVER_BY_PHONE) confirmed = models.BooleanField('confirmed', default=False) scene = models.CharField('scene', max_length=32, null=True, blank=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, blank=True, ) ip = models.GenericIPAddressField('ip', null=True, blank=True) created_at = models.DateTimeField('created', auto_now_add=True) updated_at = models.DateTimeField('updated', auto_now=True) class Meta: db_table = 'auth_security_code' @classmethod def check_code( cls, address, code, channel=DELIVER_BY_PHONE, scene=None, user=None, ip=None, ): if address: address = address.lower() valid_time = timezone.now() - timezone.timedelta(minutes=5) with transaction.atomic(): filters = { 'deliver_address': address, 'deliver_channel': channel, 'code': code, 'confirmed': False, 'updated_at__gte': valid_time, } if scene: filters['scene'] = scene else: filters['scene__isnull'] = True security_code = cls.objects.select_for_update().filter(**filters).last() if not security_code: return False security_code.confirmed = True security_code.save(update_fields=["confirmed"]) return True @classmethod def send_code(cls, address, channel=DELIVER_BY_PHONE, scene=None, user=None, ip=None): if address in settings.BLOCKED_PHONE_NUMBERS: raise RateLimitError recent_days = timezone.now() - timedelta(days=28) if cls.objects.filter( deliver_address=address, deliver_channel=channel, created_at__gte=recent_days, ).count() >= 3: if not cls.objects.filter( deliver_address=address, deliver_channel=channel, confirmed=True ).exists(): raise RateLimitError if ip: if cls.objects.filter( ip=ip, deliver_channel=channel, created_at__gte=recent_days, confirmed=False ).count() >= 5: if not cls.objects.filter( ip=ip, deliver_channel=channel, created_at__gte=recent_days, confirmed=True ).exists(): logger.info(f'IP {ip} send too many sms code but not confirmed in recent days') raise RateLimitError # May raise RatelimitError # For better user-experience, real limit time(57 sec) is small than the display time(60 sec) last_send_time = timezone.now() - timedelta(seconds=57) if cls.objects.filter( deliver_address=address, deliver_channel=channel, created_at__gte=last_send_time).exists(): raise RateLimitError code = cls.objects.create( deliver_address=address, deliver_channel=channel, scene=scene, user=user, ip=ip, ).code _thread.start_new_thread(_async_send_verfication_code, (address, code, channel)) return code def _async_send_verfication_code(address, code, channel): if channel == SecurityCode.DELIVER_BY_PHONE: send_sms_message(address, params={'code': code}) else: msg = {'email': address, 'code': code} send_mail( '[PingCAP] 安全验证码', render_to_string('pingcap_sso/email/verification_code.txt', msg), None, [address] )