feat(auth): 小程序扫码登录其它端口 —— 生成小程序码(微信扫一扫自动拉起)
- wechat.get_wxacode(access_token缓存+getwxacodeunlimit),scene短token<=32 - /auth/mp-qr/start 返回 qr_image(小程序码,扫码自动打开小程序pages/scan-login) ,无凭据回退 qr_url - mp-qr gate 改 AUTH_WECHAT_QR(默认开)
This commit is contained in:
@@ -110,6 +110,69 @@ async def oauth_code2openid(code: str) -> str:
|
||||
return openid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 小程序码(wxacode)—— 微信「扫一扫」自动打开小程序到指定页面 + 携带 scene
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_WX_ACODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={token}"
|
||||
_WX_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}"
|
||||
|
||||
# 缓存 access_token(模块级,线程安全;微信 token 有效期约 7200s)
|
||||
_token_cache: dict = {"token": "", "expires_at": 0.0}
|
||||
_token_lock = threading.Lock()
|
||||
|
||||
|
||||
async def _wx_access_token() -> str:
|
||||
"""获取小程序 access_token(缓存)。未配置 WECHAT_APPID/SECRET 时抛 WechatError。"""
|
||||
if not config.WECHAT_APPID or not config.WECHAT_SECRET:
|
||||
raise WechatError("WECHAT_APPID/WECHAT_SECRET 未配置,无法生成小程序码")
|
||||
now = time.monotonic()
|
||||
with _token_lock:
|
||||
if _token_cache["token"] and now < _token_cache["expires_at"]:
|
||||
return _token_cache["token"]
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.get(
|
||||
_WX_TOKEN_URL.format(appid=config.WECHAT_APPID, secret=config.WECHAT_SECRET),
|
||||
)
|
||||
data = resp.json()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise WechatError(f"wechat access_token request failed: {exc}") from exc
|
||||
token = data.get("access_token", "")
|
||||
if not token:
|
||||
raise WechatError(f"wechat access_token error: {data.get('errmsg', data)}")
|
||||
with _token_lock:
|
||||
_token_cache["token"] = token
|
||||
_token_cache["expires_at"] = now + int(data.get("expires_in", 7200)) - 300
|
||||
return token
|
||||
|
||||
|
||||
async def get_wxacode(scene: str, page: str = "pages/scan-login/index") -> bytes:
|
||||
"""生成小程序码(wxacode)图片,扫码自动打开小程序指定页面并携带 scene。
|
||||
|
||||
scene 限 32 字符(本服务用短 token)。返回 PNG 字节。
|
||||
"""
|
||||
token = await _wx_access_token()
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.post(
|
||||
_WX_ACODE_URL.format(token=token),
|
||||
json={"scene": scene, "page": page, "check_path": False, "env_version": "release"},
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise WechatError(f"wechat getwxacodeunlimit failed: {exc}") from exc
|
||||
if resp.status_code != 200 or resp.headers.get("content-type", "").startswith("application/json"):
|
||||
# 出错时返回 JSON 错误体
|
||||
try:
|
||||
err = resp.json()
|
||||
raise WechatError(f"wechat getwxacodeunlimit error: {err.get('errcode')} {err.get('errmsg','')}")
|
||||
except WechatError:
|
||||
raise
|
||||
except Exception:
|
||||
raise WechatError("wechat getwxacodeunlimit returned non-image")
|
||||
return resp.content
|
||||
|
||||
|
||||
class WxQrSessionStore:
|
||||
"""扫码登录会话:scene → {status, token, profile, expires_at}(内存 + TTL)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user