modify: auth插件拆分,将token签发部分交给网关
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from .github import github_authorize, github_callback # noqa: F401
|
||||
from .phone import phone_send_code, phone_login # noqa: F401
|
||||
@@ -0,0 +1,86 @@
|
||||
"""GitHub OAuth 授权码模式端点(SPA 友好的 JSON 流程)。
|
||||
|
||||
流程:前端调 authorize 拿 authorize_url → 浏览器跳转 GitHub →
|
||||
GitHub 回跳前端页(带 code+state)→ 前端调 callback 换平台 token。
|
||||
"""
|
||||
import logging
|
||||
import secrets
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
from ..models import GithubAuthorizeGetResponse, GithubCallbackPostRequest, LoginPostResponse
|
||||
from ..providers import github as github_provider
|
||||
from .. import store
|
||||
|
||||
logger = logging.getLogger("plugins.auth_oauth.github")
|
||||
|
||||
_STATE_TTL = 300 # state 有效期(秒),一次性消费防 CSRF
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth_oauth.github_authorize",
|
||||
method="GET",
|
||||
summary="GitHub 授权登录:生成授权跳转 URL(state 存缓存防 CSRF)",
|
||||
)
|
||||
async def github_authorize() -> GithubAuthorizeGetResponse:
|
||||
from ..global_var import cache, plugin_config
|
||||
|
||||
gh = plugin_config.get("github") or {}
|
||||
client_id = gh.get("client_id", "")
|
||||
if not client_id or client_id.startswith("your-"):
|
||||
return GithubAuthorizeGetResponse(
|
||||
result="fail", authorize_url="",
|
||||
state="GitHub OAuth 未配置(config/workers/auth_oauth/config.toml 的 [workers.config.github])",
|
||||
)
|
||||
|
||||
state = secrets.token_urlsafe(24)
|
||||
cache.set(f"auth_oauth:state:{state}", "1", expire=_STATE_TTL)
|
||||
|
||||
params = urlencode({
|
||||
"client_id": client_id,
|
||||
"redirect_uri": gh.get("redirect_uri", ""),
|
||||
"scope": gh.get("scope", "read:user user:email"),
|
||||
"state": state,
|
||||
})
|
||||
return GithubAuthorizeGetResponse(
|
||||
result="success",
|
||||
authorize_url=f"https://github.com/login/oauth/authorize?{params}",
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth_oauth.github_callback",
|
||||
method="POST",
|
||||
summary="GitHub 授权回调:校验 state、换取用户信息,find-or-create 后签发平台 token",
|
||||
)
|
||||
async def github_callback(req: GithubCallbackPostRequest) -> LoginPostResponse:
|
||||
from ..global_var import cache, db, plugin_config
|
||||
|
||||
if db is None or cache is None:
|
||||
return store.fail_response("插件未初始化")
|
||||
|
||||
# state 一次性消费(防 CSRF / 重放)
|
||||
state_key = f"auth_oauth:state:{req.state}"
|
||||
if not cache.get(state_key):
|
||||
return store.fail_response("state 无效或已过期,请重新发起授权")
|
||||
cache.delete(state_key)
|
||||
|
||||
gh = plugin_config.get("github") or {}
|
||||
try:
|
||||
access_token = await github_provider.exchange_code(
|
||||
gh.get("client_id", ""), gh.get("client_secret", ""),
|
||||
req.code, gh.get("redirect_uri", ""),
|
||||
)
|
||||
profile = await github_provider.fetch_user(access_token)
|
||||
except RuntimeError as e:
|
||||
logger.warning("GitHub 回调失败: %s", e)
|
||||
return store.fail_response(str(e))
|
||||
|
||||
user, message = await store.find_or_create_user(
|
||||
db, "github", profile["uid"], profile
|
||||
)
|
||||
if user is None:
|
||||
return store.fail_response(message)
|
||||
return store.issue_login_response(user)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""手机验证码登录端点(演示模式:验证码存共享缓存、默认随响应返回)。
|
||||
|
||||
生产接入真实短信通道后,将 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)
|
||||
Reference in New Issue
Block a user