1. 下载与运行
Demo 是一个单文件桌面程序(约 700 行),把「申请二维码参数 → 取 uuid → 长轮询 → 换 ticket → 拉取用户资料」 全部放在客户端完成,适合桌面软件、内网工具或需要独立登录窗口的场景。
下载 wx_login_demo.py 下载 requirements-demo.txt
pip install -r requirements-demo.txt,需要安装 PySide6 与 requests。
# 1. 安装依赖
pip install -r requirements-demo.txt
# 2. 填入「我的项目」里的凭证与白名单回调地址
$env:WX_CLIENT_ID = "ak_你的client_id"
$env:WX_CLIENT_SECRET = "sk_你的client_secret"
$env:WX_REDIRECT_URL = "https://你的站点/callback"
# 3. 启动
python wx_login_demo.py
set 换成 export 即可,例如
export WX_CLIENT_ID=ak_你的client_id。
2. 配置项
| 环境变量 | 必填 | 说明 |
|---|---|---|
WX_CLIENT_ID |
是 | 项目 client_id,在「我的项目」中查看。 |
WX_CLIENT_SECRET |
是 | 项目 client_secret,仅用于最后一步换取用户资料。 |
WX_REDIRECT_URL |
是 | 必须属于项目域名白名单。它只作为 /oauth/qr 的参数,Demo 不会真正打开它。 |
WX_GATEWAY |
否 | 登录网关地址,默认 https://login.fps5.cn。 |
/oauth/callback,并关闭自动跳转,只从 302 的 Location 里取一次性 ticket,
因此不需要在 redirect_url 对应的站点上写任何 PHP / Python 代码。
3. 登录链路
桌面端没有网页上下文,所以每一步都由 Python 直接发起:
GET https://login.fps5.cn/oauth/qr,传 client_id、redirect_url 和本地生成的 state,拿到 appid / scope / 网关 state。
请求 open.weixin.qq.com/connect/qrconnect,从返回的 HTML 中提取 uuid,不加载浏览器。
下载 /connect/qrcode/{uuid} 图片,用 QPixmap 渲染到窗口。
lp.open.weixin.qq.com/connect/l/qrconnect,408 等待、404 已扫码、405 确认并返回 wx_code。
把 wx_code 与网关 state 交给 /oauth/callback,不跟随 302,从 Location 中取 ticket。
POST /api/user/info,用 client_id + client_secret + ticket 拿到 openid、昵称、头像并展示。
4. 扫码状态码
长轮询返回的 wx_errcode 是整条链路最容易踩坑的地方:
| 状态码 | 含义 | 正确处理 |
|---|---|---|
408 | 等待扫码 | 继续轮询 |
404 | 已扫码,等待手机确认 | 继续轮询,提示用户点「允许」 |
405 | 已确认授权 | 从响应中取 wx_code,进入换票 |
403 | 用户在手机取消 | 提示后继续轮询 |
402 | 二维码过期 | 停止轮询,提示刷新二维码 |
500 | 微信服务异常 | 停止轮询,提示稍后重试 |
5. 关键代码
完整实现见下载文件,核心逻辑只有三段。
5.1 申请参数并解析 uuid
payload = requests.get(
f"{GATEWAY}/oauth/qr",
params={
"client_id": CLIENT_ID,
"redirect_url": REDIRECT_URL,
"state": biz_state, # 本地随机串,回跳时校验
},
timeout=15,
).json()["data"]
qr_url = "https://open.weixin.qq.com/connect/qrconnect?" + urlencode({
"appid": payload["appid"],
"redirect_uri": payload["redirect_uri"],
"response_type": "code",
"scope": payload.get("scope") or "snsapi_login",
"state": payload["state"], # 网关内部 state,原样回传
}) + "#wechat_redirect"
html = requests.get(qr_url, timeout=20).text
uuid = re.search(r"l/qrconnect\?uuid=([A-Za-z0-9_-]+)", html).group(1)
5.2 长轮询扫码状态
last = ""
while True:
params = {"uuid": uuid}
if last:
params["last"] = last
body = requests.get(
"https://lp.open.weixin.qq.com/connect/l/qrconnect",
params=params,
timeout=(10, 40), # 微信会挂起约 15 秒
).text
errcode = int(re.search(r"wx_errcode\s*=\s*(\d+)", body).group(1))
code = re.search(r"wx_code\s*=\s*'([^']*)'", body).group(1)
if code: # 405 确认,拿到授权码
break
if errcode == 404: # 已扫码,继续等手机确认
show_tip("扫描成功,请在手机上点击「允许」")
elif errcode == 402: # 真正过期
raise RuntimeError("二维码已过期,请刷新")
last = str(errcode)
5.3 换 ticket 并拉取用户资料
# 关键:不跟随 302,只读 Location
resp = requests.get(
f"{GATEWAY}/oauth/callback",
params={"code": wx_code, "state": gateway_state},
allow_redirects=False,
timeout=20,
)
location = resp.headers["Location"] # https://你的站点/callback?code=TICKET&state=...
query = parse_qs(urlsplit(location).query)
ticket = query["code"][0]
if query.get("state", [""])[0] != biz_state:
raise RuntimeError("state 校验失败")
user = requests.post(
f"{GATEWAY}/api/user/info",
data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": ticket,
},
timeout=20,
).json()["data"]
print(user["openid"], user["nickname"], user["avatar"])
6. 常见问题
启动后提示「请先设置环境变量」
说明 client_id、client_secret 或 redirect_url 没读到。确认环境变量是在同一个终端里设置的,再执行 python wx_login_demo.py。
扫码后一直不跳转
先看窗口提示:显示「扫描成功,请在手机上点击「允许」」说明轮询正常,等手机确认即可。若手机已点允许仍无反应,查看日志 %TEMP%\wx_login_demo.log(macOS / Linux 为系统临时目录),确认轮询是否收到 405、网关回调是否返回 302。
提示 state 校验失败
通常是重复使用了旧二维码,或回调地址被其他页面接管。点击「刷新二维码」重新发起即可;Demo 每次刷新都会生成新的 state。
提示域名不在白名单
WX_REDIRECT_URL 的域名必须与项目里配置的域名一致。修改项目域名或回调地址后需要重新审核。
能打包成 exe 吗
可以,用 PyInstaller 打包即可。但 client_secret 会随程序分发,建议正式产品改为「客户端 → 自己的后端 → 网关」,由后端持有 secret 并完成换票。