up
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"""认证服务:验证码 / 注册 / 登录 / 刷新 / 登出 / 找回密码"""
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import smtplib
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
import httpx
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -66,14 +70,90 @@ async def send_code(
|
||||
await redis.expire(ip_key, 3600)
|
||||
|
||||
data = {"expire_seconds": ttl, "retry_after": 60}
|
||||
# TODO: 接入阿里云短信 / SMTP 实际发送;未配置通道时按开发模式处理
|
||||
channel_configured = bool(settings.sms_access_key_id) if channel == "sms" else bool(settings.smtp_host)
|
||||
channel_configured = (
|
||||
bool(settings.smsbao_username and settings.smsbao_api_key)
|
||||
if channel == "sms"
|
||||
else bool(settings.smtp_host)
|
||||
)
|
||||
if not channel_configured:
|
||||
logger.info("[dev] %s code for %s (%s): %s", channel, target, scene, code)
|
||||
data["dev_code"] = code
|
||||
elif channel == "sms":
|
||||
await _send_sms_code(target, code)
|
||||
else:
|
||||
await _send_email_code(target, code, scene)
|
||||
return data
|
||||
|
||||
|
||||
_SCENES = {"register": "注册", "login": "登录", "reset": "找回密码"}
|
||||
|
||||
# ---------- 短信(短信宝)----------
|
||||
SMSBAO_SEND_URL = "https://api.smsbao.com/sms"
|
||||
# 验证码短信正文(签名前缀由 settings.sms_sign_name 提供)
|
||||
_SMS_CONTENT = "您的验证码是{code}。如非本人操作,请忽略本短信"
|
||||
# 接口文本返回码(非 0 均为失败,仅用于日志定位)
|
||||
_SMS_ERRORS = {
|
||||
"30": "密码/ApiKey 错误",
|
||||
"40": "账号不存在",
|
||||
"41": "余额不足",
|
||||
"42": "账号已过期",
|
||||
"43": "IP 地址限制",
|
||||
"50": "内容含有敏感词",
|
||||
"51": "手机号码不正确",
|
||||
"60": "尚未充值,接口不可调用",
|
||||
}
|
||||
|
||||
|
||||
async def _send_sms_code(target: str, code: str) -> None:
|
||||
"""短信宝发送验证码:返回 '0' 视为提交成功"""
|
||||
content = f"{settings.sms_sign_name}{_SMS_CONTENT.format(code=code)}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.get(
|
||||
SMSBAO_SEND_URL,
|
||||
params={
|
||||
"u": settings.smsbao_username,
|
||||
"p": settings.smsbao_api_key,
|
||||
"m": target,
|
||||
"c": content, # httpx 自动按 UTF-8 URL Encode
|
||||
},
|
||||
)
|
||||
result = resp.text.strip()
|
||||
except Exception:
|
||||
logger.exception("smsbao request failed: %s", target)
|
||||
raise BizException(errors.PARAM_ERROR, "验证码发送失败,请稍后重试")
|
||||
if result != "0":
|
||||
logger.error(
|
||||
"smsbao send failed: mobile=%s result=%s(%s)",
|
||||
target, result, _SMS_ERRORS.get(result, "未知错误"),
|
||||
)
|
||||
raise BizException(errors.PARAM_ERROR, "验证码发送失败,请稍后重试")
|
||||
|
||||
|
||||
def _smtp_send(target: str, code: str, scene: str) -> None:
|
||||
"""SMTP 发送验证码(同步 smtplib,由调用方放线程池)"""
|
||||
msg = MIMEText(
|
||||
f"您的{_SCENES.get(scene, scene)}验证码:{code},10 分钟内有效。"
|
||||
"如非本人操作请忽略本邮件。"
|
||||
)
|
||||
msg["Subject"] = f"短视频{_SCENES.get(scene, scene)}验证码"
|
||||
msg["From"] = settings.smtp_user
|
||||
msg["To"] = target
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=15) as server:
|
||||
server.starttls()
|
||||
server.login(settings.smtp_user, settings.smtp_password)
|
||||
server.send_message(msg)
|
||||
|
||||
|
||||
async def _send_email_code(target: str, code: str, scene: str) -> None:
|
||||
"""SMTP 异步包装:发送失败视为验证码错误前置(用户需重发)"""
|
||||
try:
|
||||
await asyncio.to_thread(_smtp_send, target, code, scene)
|
||||
except Exception:
|
||||
logger.exception("smtp send failed: %s", target)
|
||||
raise BizException(errors.PARAM_ERROR, "验证码发送失败,请稍后重试")
|
||||
|
||||
|
||||
async def _verify_code(redis: aioredis.Redis, channel: str, target: str, scene: str, code: str) -> None:
|
||||
key = _code_key(scene, channel, target)
|
||||
saved = await redis.get(key)
|
||||
|
||||
Reference in New Issue
Block a user