mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
feat(auth): 添加第三方OAuth登录支持
- 实现OAuth登录重定向和回调处理 - 更新配置文件以支持多种OAuth提供商 - 添加错误处理和状态管理功能 - 更新文档以反映新功能和配置要求
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import json
|
||||
import secrets
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import ErrorResponse, SuccessResponse
|
||||
from app.config.setting import settings
|
||||
from app.core.dependencies import db_getter, get_current_user, redis_getter
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.security import CustomOAuth2PasswordRequestForm
|
||||
@@ -20,6 +24,15 @@ from .schema import (
|
||||
LogoutPayloadSchema,
|
||||
RefreshTokenPayloadSchema,
|
||||
)
|
||||
from .oauth_service import (
|
||||
STATE_PREFIX,
|
||||
build_authorize_url,
|
||||
complete_oauth_login,
|
||||
oauth_service_error_redirect,
|
||||
oauth_service_frontend_redirect_from_token,
|
||||
save_oauth_state,
|
||||
_callback_url,
|
||||
)
|
||||
from .service import AutoLoginService, CaptchaService, LoginService
|
||||
|
||||
AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"])
|
||||
@@ -236,3 +249,100 @@ async def auto_login_controller(
|
||||
)
|
||||
log.info("用户免登录成功")
|
||||
return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
|
||||
|
||||
|
||||
@AuthRouter.get(
|
||||
"/oauth/{provider}/login",
|
||||
summary="第三方OAuth跳转",
|
||||
description="浏览器重定向到微信/GitHub/Gitee/QQ 授权页;redirect_uri 为授权完成后回到前端的登录页地址(如 http://localhost:5173/login)。",
|
||||
)
|
||||
async def oauth_login_redirect_controller(
|
||||
request: Request,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
provider: Annotated[str, Path(description="wechat | qq | github | gitee")],
|
||||
redirect_uri: Annotated[
|
||||
str | None,
|
||||
Query(description="OAuth 完成后浏览器回到的前端登录页完整 URL"),
|
||||
] = None,
|
||||
) -> RedirectResponse:
|
||||
allowed = {"wechat", "qq", "github", "gitee"}
|
||||
fe = redirect_uri or settings.OAUTH_FRONTEND_FALLBACK
|
||||
if provider not in allowed:
|
||||
return RedirectResponse(
|
||||
url=oauth_service_error_redirect(fe, "不支持的 OAuth 渠道"),
|
||||
status_code=302,
|
||||
)
|
||||
if not redirect_uri:
|
||||
return RedirectResponse(
|
||||
url=oauth_service_error_redirect(fe, "缺少 redirect_uri 参数"),
|
||||
status_code=302,
|
||||
)
|
||||
try:
|
||||
state = secrets.token_urlsafe(32)
|
||||
await save_oauth_state(
|
||||
redis=redis,
|
||||
state=state,
|
||||
provider=provider,
|
||||
frontend_redirect=redirect_uri,
|
||||
)
|
||||
cb = _callback_url(request, provider)
|
||||
url = build_authorize_url(provider=provider, callback_url=cb, state=state)
|
||||
return RedirectResponse(url=url, status_code=302)
|
||||
except CustomException as e:
|
||||
return RedirectResponse(
|
||||
url=oauth_service_error_redirect(redirect_uri, e.msg),
|
||||
status_code=302,
|
||||
)
|
||||
|
||||
|
||||
@AuthRouter.get(
|
||||
"/oauth/{provider}/callback",
|
||||
summary="第三方OAuth回调",
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def oauth_callback_controller(
|
||||
request: Request,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
provider: Annotated[str, Path()],
|
||||
code: Annotated[str | None, Query()] = None,
|
||||
state: Annotated[str | None, Query()] = None,
|
||||
) -> RedirectResponse:
|
||||
fe_fallback = settings.OAUTH_FRONTEND_FALLBACK
|
||||
|
||||
async def resolve_frontend() -> str:
|
||||
if not state:
|
||||
return fe_fallback
|
||||
raw = await RedisCURD(redis).get(f"{STATE_PREFIX}{state}")
|
||||
if not raw:
|
||||
return fe_fallback
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
return str(payload.get("frontend_redirect") or fe_fallback).strip() or fe_fallback
|
||||
except json.JSONDecodeError:
|
||||
return fe_fallback
|
||||
|
||||
if provider not in {"wechat", "qq", "github", "gitee"}:
|
||||
url = oauth_service_error_redirect(await resolve_frontend(), "不支持的 OAuth 渠道")
|
||||
return RedirectResponse(url=url, status_code=302)
|
||||
if not code or not state:
|
||||
url = oauth_service_error_redirect(
|
||||
await resolve_frontend(), "授权被取消或参数不完整"
|
||||
)
|
||||
return RedirectResponse(url=url, status_code=302)
|
||||
try:
|
||||
token, fe = await complete_oauth_login(
|
||||
request=request,
|
||||
redis=redis,
|
||||
db=db,
|
||||
provider=provider,
|
||||
code=code,
|
||||
state=state,
|
||||
)
|
||||
success_url = oauth_service_frontend_redirect_from_token(fe, token)
|
||||
return RedirectResponse(url=success_url, status_code=302)
|
||||
except CustomException as e:
|
||||
fe = await resolve_frontend()
|
||||
return RedirectResponse(url=oauth_service_error_redirect(fe, e.msg), status_code=302)
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
第三方 OAuth2 登录(微信开放平台扫码、QQ、GitHub、Gitee)。
|
||||
|
||||
各平台需在开放平台登记「授权回调域 / redirect_uri」为:
|
||||
{API}/system/auth/oauth/{provider}/callback
|
||||
例如:https://your-domain.com/api/v1/system/auth/oauth/github/callback
|
||||
|
||||
环境变量见 Settings 中 OAUTH_* 字段。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import secrets
|
||||
from typing import Any, Literal, Tuple
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_system.user.crud import UserCRUD
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
from app.api.v1.module_system.user.schema import UserRegisterSchema
|
||||
from app.api.v1.module_system.user.service import UserService
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from .schema import JWTOutSchema
|
||||
from .service import LoginService
|
||||
|
||||
OAuthProvider = Literal["wechat", "qq", "github", "gitee"]
|
||||
|
||||
STATE_PREFIX = "oauth_state:"
|
||||
STATE_TTL_SECONDS = 600
|
||||
|
||||
|
||||
def _callback_url(request: Request, provider: OAuthProvider) -> str:
|
||||
root = str(request.base_url).rstrip("/")
|
||||
return f"{root}/system/auth/oauth/{provider}/callback"
|
||||
|
||||
|
||||
def _frontend_error_redirect(frontend_base: str, message: str) -> str:
|
||||
sep = "&" if "?" in frontend_base else "?"
|
||||
return f"{frontend_base}{sep}oauth_error={quote(message, safe='')}"
|
||||
|
||||
|
||||
def _frontend_success_redirect(
|
||||
frontend_base: str, access_token: str, refresh_token: str, token_type: str
|
||||
) -> str:
|
||||
q = urlencode(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": token_type,
|
||||
}
|
||||
)
|
||||
sep = "&" if "?" in frontend_base else "?"
|
||||
return f"{frontend_base}{sep}{q}"
|
||||
|
||||
|
||||
def _require_credentials(provider: OAuthProvider) -> tuple[str, str]:
|
||||
if provider == "github":
|
||||
cid, sec = settings.OAUTH_GITHUB_CLIENT_ID, settings.OAUTH_GITHUB_CLIENT_SECRET
|
||||
elif provider == "gitee":
|
||||
cid, sec = settings.OAUTH_GITEE_CLIENT_ID, settings.OAUTH_GITEE_CLIENT_SECRET
|
||||
elif provider == "wechat":
|
||||
cid, sec = settings.OAUTH_WECHAT_OPEN_APP_ID, settings.OAUTH_WECHAT_OPEN_APP_SECRET
|
||||
elif provider == "qq":
|
||||
cid, sec = settings.OAUTH_QQ_APP_ID, settings.OAUTH_QQ_APP_SECRET
|
||||
else:
|
||||
raise CustomException(msg="不支持的 OAuth 渠道")
|
||||
if not cid or not sec:
|
||||
raise CustomException(msg=f"{provider} OAuth 未配置(客户端密钥为空)")
|
||||
return cid, sec
|
||||
|
||||
|
||||
def build_authorize_url(
|
||||
*,
|
||||
provider: OAuthProvider,
|
||||
callback_url: str,
|
||||
state: str,
|
||||
) -> str:
|
||||
"""构造跳转至第三方授权页的 URL。"""
|
||||
cid, _ = _require_credentials(provider)
|
||||
|
||||
if provider == "github":
|
||||
params = {
|
||||
"client_id": cid,
|
||||
"redirect_uri": callback_url,
|
||||
"scope": "user:email",
|
||||
"state": state,
|
||||
}
|
||||
return "https://github.com/login/oauth/authorize?" + urlencode(params)
|
||||
|
||||
if provider == "gitee":
|
||||
params = {
|
||||
"client_id": cid,
|
||||
"redirect_uri": callback_url,
|
||||
"response_type": "code",
|
||||
"state": state,
|
||||
}
|
||||
return "https://gitee.com/oauth/authorize?" + urlencode(params)
|
||||
|
||||
if provider == "wechat":
|
||||
params = {
|
||||
"appid": cid,
|
||||
"redirect_uri": callback_url,
|
||||
"response_type": "code",
|
||||
"scope": "snsapi_login",
|
||||
"state": state,
|
||||
}
|
||||
return "https://open.weixin.qq.com/connect/qrconnect?" + urlencode(params) + "#wechat_redirect"
|
||||
|
||||
if provider == "qq":
|
||||
params = {
|
||||
"response_type": "code",
|
||||
"client_id": cid,
|
||||
"redirect_uri": callback_url,
|
||||
"state": state,
|
||||
"scope": "get_user_info",
|
||||
}
|
||||
return "https://graph.qq.com/oauth2.0/authorize?" + urlencode(params)
|
||||
|
||||
raise CustomException(msg="不支持的 OAuth 渠道")
|
||||
|
||||
|
||||
async def _http_json(method: str, url: str, **kwargs: Any) -> Any:
|
||||
timeout = getattr(settings, "HTTPX_DEFAULT_TIMEOUT", 15.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
r = await client.request(method, url, **kwargs)
|
||||
r.raise_for_status()
|
||||
try:
|
||||
return r.json()
|
||||
except json.JSONDecodeError:
|
||||
text = r.text
|
||||
log.error(f"OAuth 非 JSON 响应: {text[:500]}")
|
||||
raise CustomException(msg="OAuth 接口返回异常")
|
||||
|
||||
|
||||
async def _http_text(method: str, url: str, **kwargs: Any) -> str:
|
||||
timeout = getattr(settings, "HTTPX_DEFAULT_TIMEOUT", 15.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
r = await client.request(method, url, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.text
|
||||
|
||||
|
||||
async def exchange_github_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> str:
|
||||
data = await _http_json(
|
||||
"POST",
|
||||
"https://github.com/login/oauth/access_token",
|
||||
headers={"Accept": "application/json"},
|
||||
data={
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
},
|
||||
)
|
||||
if not isinstance(data, dict):
|
||||
raise CustomException(msg="GitHub token 响应格式错误")
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
raise CustomException(msg=data.get("error_description") or "GitHub 换取令牌失败")
|
||||
return str(token)
|
||||
|
||||
|
||||
async def exchange_gitee_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> str:
|
||||
qs = urlencode(
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
)
|
||||
data = await _http_json("GET", f"https://gitee.com/oauth/token?{qs}")
|
||||
if not isinstance(data, dict):
|
||||
raise CustomException(msg="Gitee token 响应格式错误")
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
raise CustomException(msg=data.get("error_description") or "Gitee 换取令牌失败")
|
||||
return str(token)
|
||||
|
||||
|
||||
async def exchange_wechat_token(app_id: str, secret: str, code: str) -> tuple[str, str]:
|
||||
qs = urlencode(
|
||||
{
|
||||
"appid": app_id,
|
||||
"secret": secret,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
)
|
||||
data = await _http_json("GET", f"https://api.weixin.qq.com/sns/oauth2/access_token?{qs}")
|
||||
if not isinstance(data, dict):
|
||||
raise CustomException(msg="微信 token 响应格式错误")
|
||||
token = data.get("access_token")
|
||||
openid = data.get("openid")
|
||||
if not token or not openid:
|
||||
raise CustomException(msg=data.get("errmsg") or "微信换取令牌失败")
|
||||
return str(token), str(openid)
|
||||
|
||||
|
||||
async def exchange_qq_token(client_id: str, client_secret: str, code: str, redirect_uri: str) -> tuple[str, str]:
|
||||
qs = urlencode(
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
)
|
||||
text = await _http_text("GET", f"https://graph.qq.com/oauth2.0/token?{qs}")
|
||||
parts = dict(p.split("=", 1) for p in text.split("&") if "=" in p)
|
||||
token = parts.get("access_token")
|
||||
if not token:
|
||||
raise CustomException(msg="QQ 换取 access_token 失败")
|
||||
me = await _http_json(
|
||||
"GET",
|
||||
"https://graph.qq.com/oauth2.0/me",
|
||||
params={"access_token": token, "fmt": "json"},
|
||||
)
|
||||
if not isinstance(me, dict):
|
||||
raise CustomException(msg="QQ openid 响应格式错误")
|
||||
openid = me.get("openid")
|
||||
if not openid:
|
||||
raise CustomException(msg="QQ 获取 openid 失败")
|
||||
return str(token), str(openid)
|
||||
|
||||
|
||||
async def fetch_github_profile(access_token: str) -> tuple[str, str, str | None]:
|
||||
headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
|
||||
user = await _http_json("GET", "https://api.github.com/user", headers=headers)
|
||||
if not isinstance(user, dict):
|
||||
raise CustomException(msg="GitHub 用户信息格式错误")
|
||||
login = str(user.get("login") or "")
|
||||
name = str(user.get("name") or login or "github")
|
||||
email = user.get("email")
|
||||
if not email:
|
||||
emails = await _http_json("GET", "https://api.github.com/user/emails", headers=headers)
|
||||
if isinstance(emails, list):
|
||||
primary = next((e for e in emails if isinstance(e, dict) and e.get("primary")), None)
|
||||
if primary:
|
||||
email = primary.get("email")
|
||||
return login, name, email
|
||||
|
||||
|
||||
async def fetch_gitee_profile(access_token: str) -> tuple[str, str, str | None]:
|
||||
user = await _http_json(
|
||||
"GET",
|
||||
"https://gitee.com/api/v5/user",
|
||||
params={"access_token": access_token},
|
||||
)
|
||||
if not isinstance(user, dict):
|
||||
raise CustomException(msg="Gitee 用户信息格式错误")
|
||||
login = str(user.get("login") or "")
|
||||
name = str(user.get("name") or login)
|
||||
email = user.get("email")
|
||||
return login, name, email
|
||||
|
||||
|
||||
async def fetch_wechat_profile(access_token: str, openid: str) -> tuple[str, str]:
|
||||
qs = urlencode({"access_token": access_token, "openid": openid, "lang": "zh_CN"})
|
||||
user = await _http_json("GET", f"https://api.weixin.qq.com/sns/userinfo?{qs}")
|
||||
if not isinstance(user, dict):
|
||||
raise CustomException(msg="微信用户信息格式错误")
|
||||
nickname = str(user.get("nickname") or "wechat")
|
||||
unionid = user.get("unionid")
|
||||
oid = unionid or openid
|
||||
return str(oid), nickname
|
||||
|
||||
|
||||
async def fetch_qq_profile(access_token: str, app_id: str, openid: str) -> tuple[str, str]:
|
||||
qs = urlencode(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"oauth_consumer_key": app_id,
|
||||
"openid": openid,
|
||||
}
|
||||
)
|
||||
user = await _http_json("GET", f"https://graph.qq.com/user/get_user_info?{qs}")
|
||||
if not isinstance(user, dict):
|
||||
raise CustomException(msg="QQ 用户信息格式错误")
|
||||
if user.get("ret") not in (0, "0", None):
|
||||
raise CustomException(msg=user.get("msg") or "QQ 用户信息失败")
|
||||
nickname = str(user.get("nickname") or "qq")
|
||||
return openid, nickname
|
||||
|
||||
|
||||
def _username_for_oauth(provider: OAuthProvider, unique_id: str) -> str:
|
||||
"""生成符合注册规则的登录名:oauth_{provider}_{id}。"""
|
||||
raw = f"oauth_{provider}_{unique_id}"
|
||||
raw = "".join(c if c.isalnum() or c in "_-." else "_" for c in raw)[:32]
|
||||
if len(raw) < 3:
|
||||
raw = (raw + "usr")[:32]
|
||||
if not raw[0].isalpha():
|
||||
raw = "o" + raw[:31]
|
||||
return raw
|
||||
|
||||
|
||||
async def ensure_oauth_user(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
provider: OAuthProvider,
|
||||
unique_id: str,
|
||||
display_name: str,
|
||||
) -> UserModel:
|
||||
auth = AuthSchema(db=db, user=None, check_data_scope=False)
|
||||
username = _username_for_oauth(provider, unique_id)
|
||||
existing = await UserCRUD(auth).get_by_username_crud(username=username)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
reg = UserRegisterSchema(
|
||||
username=username,
|
||||
password=secrets.token_urlsafe(24),
|
||||
name=(display_name or username)[:32],
|
||||
role_ids=list(settings.OAUTH_DEFAULT_ROLE_IDS),
|
||||
)
|
||||
await UserService.register_user_service(auth=auth, data=reg)
|
||||
user = await UserCRUD(auth).get_by_username_crud(username=username)
|
||||
if not user:
|
||||
raise CustomException(msg="OAuth 注册失败")
|
||||
log.info(f"OAuth 自动注册用户: {username} ({provider})")
|
||||
return user
|
||||
|
||||
|
||||
async def complete_oauth_login(
|
||||
*,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
provider: OAuthProvider,
|
||||
code: str,
|
||||
state: str,
|
||||
) -> Tuple[JWTOutSchema, str]:
|
||||
rc = RedisCURD(redis)
|
||||
raw = await rc.get(f"{STATE_PREFIX}{state}")
|
||||
if not raw:
|
||||
raise CustomException(msg="登录状态已失效,请重试")
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
payload = json.loads(raw)
|
||||
if payload.get("provider") != provider:
|
||||
raise CustomException(msg="OAuth 状态不匹配")
|
||||
|
||||
frontend = str(payload.get("frontend_redirect") or "").strip()
|
||||
if not frontend:
|
||||
raise CustomException(msg="缺少前端回调地址")
|
||||
|
||||
callback_url = _callback_url(request, provider)
|
||||
cid, csec = _require_credentials(provider)
|
||||
|
||||
if provider == "github":
|
||||
access = await exchange_github_token(cid, csec, code, callback_url)
|
||||
login_k, name, _email = await fetch_github_profile(access)
|
||||
uid = login_k
|
||||
elif provider == "gitee":
|
||||
access = await exchange_gitee_token(cid, csec, code, callback_url)
|
||||
login_k, name, _email = await fetch_gitee_profile(access)
|
||||
uid = login_k
|
||||
elif provider == "wechat":
|
||||
access, openid = await exchange_wechat_token(cid, csec, code)
|
||||
uid, name = await fetch_wechat_profile(access, openid)
|
||||
elif provider == "qq":
|
||||
access, openid = await exchange_qq_token(cid, csec, code, callback_url)
|
||||
uid, name = await fetch_qq_profile(access, cid, openid)
|
||||
else:
|
||||
raise CustomException(msg="不支持的 OAuth 渠道")
|
||||
|
||||
user = await ensure_oauth_user(db=db, provider=provider, unique_id=uid, display_name=name)
|
||||
if user.status == "1":
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
user = await UserCRUD(AuthSchema(db=db, user=None, check_data_scope=False)).update_last_login_crud(
|
||||
id=user.id
|
||||
)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
login_type = f"oauth_{provider}"
|
||||
token = await LoginService.create_token_service(
|
||||
request=request, redis=redis, user=user, login_type=login_type
|
||||
)
|
||||
await rc.delete(f"{STATE_PREFIX}{state}")
|
||||
return token, frontend
|
||||
|
||||
|
||||
async def save_oauth_state(
|
||||
*,
|
||||
redis: Redis,
|
||||
state: str,
|
||||
provider: OAuthProvider,
|
||||
frontend_redirect: str,
|
||||
) -> None:
|
||||
rc = RedisCURD(redis)
|
||||
ok = await rc.set(
|
||||
f"{STATE_PREFIX}{state}",
|
||||
json.dumps({"provider": provider, "frontend_redirect": frontend_redirect}),
|
||||
expire=STATE_TTL_SECONDS,
|
||||
)
|
||||
if not ok:
|
||||
raise CustomException(msg="缓存 OAuth 状态失败")
|
||||
|
||||
|
||||
def oauth_service_frontend_redirect_from_token(
|
||||
frontend_base: str, token: JWTOutSchema
|
||||
) -> str:
|
||||
return _frontend_success_redirect(
|
||||
frontend_base,
|
||||
token.access_token,
|
||||
token.refresh_token,
|
||||
token.token_type,
|
||||
)
|
||||
|
||||
|
||||
def oauth_service_error_redirect(frontend_base: str, message: str) -> str:
|
||||
return _frontend_error_redirect(frontend_base, message)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OAuthProvider",
|
||||
"STATE_PREFIX",
|
||||
"build_authorize_url",
|
||||
"complete_oauth_login",
|
||||
"save_oauth_state",
|
||||
"_callback_url",
|
||||
"oauth_service_frontend_redirect_from_token",
|
||||
"oauth_service_error_redirect",
|
||||
]
|
||||
Reference in New Issue
Block a user