"""手机验证码登录端点(演示模式:验证码存共享缓存、默认随响应返回)。 生产接入真实短信通道后,将 send_code 中的 dev_code 分支关闭 (config: return_otp_in_response = false),验证码只经短信下发。 """ import logging import re import secrets from chongming_worker import rpc_method from ..models import ( LoginPostResponse, PhoneLoginPostRequest, PhoneSendCodePostRequest, PhoneSendCodePostResponse, ) from .. import store logger = logging.getLogger("plugins.auth_oauth.phone") _OTP_TTL = 300 # 验证码有效期(秒) _PHONE_RE = re.compile(r"^1\d{10}$") # 演示按中国大陆手机号校验 def _otp_key(phone: str) -> str: return f"auth_oauth:otp:{phone}" @rpc_method( subject="auth_oauth.phone_send_code", method="POST", summary="手机验证码登录:下发验证码(演示模式:存缓存并随响应返回)", ) async def phone_send_code(req: PhoneSendCodePostRequest) -> PhoneSendCodePostResponse: from ..global_var import cache, plugin_config if cache is None: return PhoneSendCodePostResponse(result="fail", message="插件未初始化") if not _PHONE_RE.match(req.phone): return PhoneSendCodePostResponse(result="fail", message="手机号格式不正确") code = f"{secrets.randbelow(1_000_000):06d}" cache.set(_otp_key(req.phone), code, expire=_OTP_TTL) # 生产环境在此接真实短信通道;演示模式打印并将验证码随响应返回 logger.info("[auth_oauth] 验证码已生成 phone=%s code=%s(演示模式,请勿用于生产)", req.phone, code) if plugin_config.get("return_otp_in_response", True): return PhoneSendCodePostResponse( result="success", message="验证码已发送(演示模式直接返回)", dev_code=code, ) return PhoneSendCodePostResponse(result="success", message="验证码已发送") @rpc_method( subject="auth_oauth.phone_login", method="POST", summary="手机验证码登录:校验验证码,find-or-create 后签发平台 token", ) async def phone_login(req: PhoneLoginPostRequest) -> LoginPostResponse: from ..global_var import cache, db if db is None or cache is None: return store.fail_response("插件未初始化") key = _otp_key(req.phone) stored = cache.get(key) if not stored or stored != req.code: return store.fail_response("验证码错误或已过期") cache.delete(key) # 一次性消费 user, message = await store.find_or_create_user( db, "phone", req.phone, profile={ "username": req.phone, "email": f"{req.phone}@phone.local", }, ) if user is None: return store.fail_response(message) return store.issue_login_response(user)