接入微信一键登录服务端接口
改动内容:新增微信 code 换 openid 集成、/api/v1/auth/wechat-login 登录接口、微信用户创建/复用逻辑和测试覆盖。 验证方式:python -m pytest tests\\test_auth.py tests\\test_health.py -q 通过。
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""微信开放平台 App 登录 OAuth 封装。
|
||||
|
||||
客户端只传微信 SDK 返回的临时 code;AppSecret、access_token、refresh_token 都只留在服务端。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
WECHAT_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
WECHAT_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo"
|
||||
|
||||
|
||||
class WechatLoginError(Exception):
|
||||
"""微信登录链路失败,由 auth endpoint 转成 502/503。"""
|
||||
|
||||
|
||||
def _ensure_configured() -> None:
|
||||
if not settings.WECHAT_APP_ID or not settings.WECHAT_APP_SECRET:
|
||||
raise WechatLoginError("wechat app not configured")
|
||||
|
||||
|
||||
def _json(resp: httpx.Response) -> dict[str, Any]:
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError as e:
|
||||
raise WechatLoginError("微信授权响应格式异常") from e
|
||||
if not isinstance(data, dict):
|
||||
raise WechatLoginError("微信授权响应格式异常")
|
||||
return data
|
||||
|
||||
|
||||
def code_to_userinfo(code: str) -> dict[str, Any]:
|
||||
"""用客户端授权 code 换 openid/unionid,并尽力拉取微信昵称头像。"""
|
||||
_ensure_configured()
|
||||
try:
|
||||
with httpx.Client(timeout=8.0) as client:
|
||||
token_resp = client.get(
|
||||
WECHAT_TOKEN_URL,
|
||||
params={
|
||||
"appid": settings.WECHAT_APP_ID,
|
||||
"secret": settings.WECHAT_APP_SECRET,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
token_data = _json(token_resp)
|
||||
if token_data.get("errcode"):
|
||||
raise WechatLoginError(f"微信授权失败: {token_data.get('errmsg', token_data)}")
|
||||
|
||||
access_token = token_data.get("access_token")
|
||||
openid = token_data.get("openid")
|
||||
if not access_token or not openid:
|
||||
raise WechatLoginError("微信授权响应缺少 openid")
|
||||
|
||||
info: dict[str, Any] = {
|
||||
"openid": openid,
|
||||
"unionid": token_data.get("unionid"),
|
||||
"nickname": None,
|
||||
"avatar_url": None,
|
||||
"raw": {"token": token_data},
|
||||
}
|
||||
|
||||
# userinfo 失败不阻断登录:登录主身份是 openid,昵称头像只是展示增强。
|
||||
userinfo_resp = client.get(
|
||||
WECHAT_USERINFO_URL,
|
||||
params={
|
||||
"access_token": access_token,
|
||||
"openid": openid,
|
||||
"lang": "zh_CN",
|
||||
},
|
||||
)
|
||||
userinfo = _json(userinfo_resp)
|
||||
if not userinfo.get("errcode"):
|
||||
info["unionid"] = userinfo.get("unionid") or info["unionid"]
|
||||
info["nickname"] = userinfo.get("nickname")
|
||||
info["avatar_url"] = userinfo.get("headimgurl")
|
||||
info["raw"]["userinfo"] = userinfo
|
||||
|
||||
return info
|
||||
except WechatLoginError:
|
||||
raise
|
||||
except httpx.HTTPError as e:
|
||||
raise WechatLoginError("微信授权请求失败,请稍后重试") from e
|
||||
Reference in New Issue
Block a user