83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""手机验证码登录端点(演示模式:验证码存共享缓存、默认随响应返回)。
|
||||
|
|
|
|||
|
|
生产接入真实短信通道后,将 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)
|