Files
qiji/docs/wecom-oauth-lessons.md
2026-07-30 12:31:00 +08:00

154 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 企业微信 OAuth 静默登录 — 踩坑记录
## 目标
用户在企微内点击菜单 → 自动登录 → 直达应用首页,零手动操作。
## 架构
```
企微菜单(view) → OAuth authorize URL(snsapi_base)
→ 企微静默获取 code
→ 302 到 callback URL
→ 后端换 wecom_userid → 查 DB → 签 JWT
→ 返回 HTML 页面(JS 写 localStorage + 跳转)
→ 前端启动 → auth store 读到 localStorage → 已登录
```
## 踩坑清单
### 坑 1`redirect_uri` 不能夹带自定义 query 参数
**错误做法:**
```
redirect_uri = https://xxx.com/api/callback?redirect=/m
```
企微 OAuth 完成后会在 `redirect_uri` 后追加 `?code=xxx&state=xxx`,但可能覆盖或丢弃原有的 query 参数。
**正确做法:** 把自定义参数编码到 `state` 里:
```python
state = f"r={urllib.parse.quote('/m')}"
redirect_uri = "https://xxx.com/api/callback" # 干净,无额外 query
```
回调时从 `state` 中解析:
```python
redirect = "/m"
if state.startswith("r="):
redirect = urllib.parse.unquote(state[2:])
```
### 坑 2:必须配置可信域名
企微后台 → 应用管理 → 网页授权及 JS-SDK → 设置可信域名。
不加的话 OAuth 直接报错:`redirect_uri 需使用应用可信域名`
验证文件(`WW_verify_xxx.txt`)需要放在前端可访问的静态目录下。
### 坑 3`agentid` 参数
内部应用使用 `snsapi_base` 静默授权时,OAuth URL 必须带 `agentid`
```
?appid=CORPID&agentid=AGENTID&redirect_uri=...
```
不加的话企微可能静默授权失败(表现:跳到登录页而不是自动登录)。
### 坑 4:服务端 302/307 重定向在 webview 中不可靠
**这是最大的坑。** 后端回调返回 HTTP 重定向(307/302),企微内置浏览器可能不跟进,或跟进时丢掉 query 参数。
**解决方案:** 回调返回 HTML 页面,用 JS 做客户端跳转。
```python
html = """<!DOCTYPE html><html><head><meta charset="utf-8"></head>
<body><script>
var d = {auth_data_json};
localStorage.setItem('token', d.token);
localStorage.setItem('userId', d.userId);
localStorage.setItem('userName', d.userName);
localStorage.setItem('userRole', d.userRole);
window.location.replace({frontend_url_json});
</script></body></html>"""
return HTMLResponse(content=html, status_code=200)
```
**为什么直接写 localStorage 而不是 URL 传 token**
- URL `?token=xxx` 需要前端路由守卫解析 JWT`atob` 解码)
- 企微 webview 中 `atob` 可能不可靠
- `localStorage` 是浏览器原生 API,前端 auth store 启动时直接读取
### 坑 5:用户名字符串中的特殊字符
如果直接把用户名拼进 JS 字符串,包含 `'` `"` `\` 会破坏 JS 语法。
**安全做法:** 整个 auth data 用 `json.dumps` 序列化后直接作为 JS 对象字面量:
```python
import json
auth_data = json.dumps({
"token": token,
"userId": str(user.id),
"userName": user.name, # json.dumps 会自动转义特殊字符
"userRole": user.role,
"theme": user.theme or "editorial",
}, ensure_ascii=False)
# 在 HTML 模板中:
# var d = {auth_data}; ← 直接是 JS 对象,不需要 JSON.parse
```
### 坑 6`wecom_userid` 大小写
数据库中存的 `wecom_userid` 可能与 OAuth 返回的大小写不一致(如 `WeiJueSen` vs `weijuesen`)。
**修复:** 查询时做大小写不敏感匹配:
```python
from sqlalchemy import func
result = await db.execute(
select(User).where(func.lower(User.wecom_userid) == wecom_userid.lower())
)
```
### 坑 7Login 页面死循环
Login.vue 检测到企微环境后自动跳 OAuth,但如果 OAuth 回调又回到 Login 页,就形成死循环。
**原因分析:**
- OAuth 成功 → 回调 → 写 localStorage → 跳到首页
- 如果 localStorage 写入失败或 token 无效 → 路由守卫 → /login
- Login 检测 isWecom() → 又跳 OAuth → 死循环
**防护:** 在 Login 页跳 OAuth 前检查是否刚从 OAuth 回来(如 URL 带 `?from_wecom=1`),避免重复跳转。本项目通过 localStorage 写入 + 前端 auth store 从 localStorage 初始化避开了这个问题。
## 调试技巧
1. **后端日志看回调是否被触发:** `grep oauth-callback /var/log/app.log`
2. **HTTP 状态码:** 回调返回 307 说明走到了 RedirectResponse,返回 200 说明走了 HTML 方案
3. **检查绑定状态:** 查数据库确认 `users.wecom_userid` 有值
4. **OAuth URL 自测:** 浏览器直接访问 `/api/wecom/oauth-url`,看 302 跳转的目标 URL 是否正确
## 完整代码量
| 文件 | 新增行数 | 说明 |
|------|---------|------|
| 后端 OAuth 回调 | ~30 行 | `_build_oauth_url` + `oauth-callback` 端点 |
| 前端路由守卫 | ~15 行 | 处理 `?token=` 参数自动登录(本方案中作为 fallback) |
| 前端 Login 页 | ~8 行 | 企微环境检测 + 跳 OAuth |
## 最终效果
用户视角:企微点击「开始填报」→ 白屏 0.5 秒 → 首页。
技术视角:
```
点菜单 → OAuth(snsapi_base静默) → 回调HTML → JS写localStorage → 跳首页 → 已登录
```