# 微信登录 SDK

轻量级前端认证 SDK — 弹窗内置小程序扫码登录：

- **小程序扫码**：网页展示小程序码 → 微信扫码 → 小程序内点「确认登录」→ 网页自动登录，全程无需输入验证码

> **⚠️ 公众号验证码登录已临时停用（2026-09-16）**
> 弹窗不再展示「公众号登录」Tab 与 6 位验证码输入面板，也不再向后端拉取公众号名称 / 关注二维码（`/api/sdk/config`）。
> SDK 保留 `WxAuth.verifyCode()` 与 `switchMode()` 等方法及原面板实现（源码中以「公众号验证码登录临时停用」注释块标注），
> 恢复时按注释还原即可。后端 `GET /api/auth/check?authToken=` 验证码通道未改动。
> 停用期间的降级行为：出码失败不再切到验证码 Tab，而是在扫码面板内给出「刷新二维码」重试入口。

基于 Vite 构建，**零依赖，gzip 后 < 10 KB**。

---

## 安装

```bash
npm install wx-auth-sdk
```

---

## 快速开始（零配置）

```typescript
import { WxAuth } from 'wx-auth-sdk';
import 'wx-auth-sdk/dist/wx-auth.css';

WxAuth.init({
  onVerified: (user) => {
    console.log('认证成功', user);
  }
});
```

SDK 会自动：

- 从 `document.referrer` 或当前域名自动获取站点标识（`siteId`，无需配置）
- 使用内置 API 地址

---

## 登录方式：小程序扫码（唯一）

> **⚠️ 2026-09-16 起公众号验证码登录临时停用**：弹窗不再有「小程序登录 / 公众号登录」Tab 切换，
> 打开即渲染小程序扫码面板并立即出码。

```
┌──────────────────────────────────┐
│  微信登录                      [×] │
│                                  │
│  （小程序码，打开弹窗即出码）       │
└──────────────────────────────────┘
```

### 小程序扫码（原「Tab 一」）

网页出码 → 用户微信扫码打开小程序确认页 → 点「确认登录」→ 网页每 2 秒轮询，确认后自动完成登录。

```
网页弹窗展示小程序码
        │
        ▼
用户微信扫码 → 打开小程序确认页
        │
        ▼
小程序内展示目标站点 → 用户点「确认登录」
        │
        ▼
网页轮询命中 → 领取签名 Token → onVerified() ✅
```

要点：

- **无需输入验证码**，也不用关注公众号（小程序用户与公众号用户是两条独立身份记录，见下方 `user.type`）
- 二维码 **5 分钟有效**，过期后画面盖「二维码已过期」遮罩，点「刷新二维码」重新出码
- **主要面向桌面端浏览器**——手机屏幕上的二维码无法用同一部微信长按识别；手机端可截图后用微信「扫一扫 → 相册」识别，或在微信内打开页面时长按识别
- 出码失败（后端未配置小程序 / 微信接口异常）**不再回落公众号验证码 Tab**（该面板已临时停用），而是停留在扫码面板：收起 loading、盖「二维码生成失败，请点下方按钮重试」遮罩 + 提示，并提供「刷新二维码」重试入口

### （已临时停用）公众号验证码

传统方式：微信扫码关注公众号 → 向公众号发送"验证码" → 输入收到的 6 位数字 → 认证成功。

> ⚠️ 2026-09-16 起该面板不再渲染。源码中对应实现（`tabsHtml` / `codePanelHtml`、验证码输入框与登录按钮重置、
> `/api/sdk/config` 配置拉取、出码失败回落）均以「公众号验证码登录临时停用」注释块保留，恢复时按注释还原。

---

## 登录态与 Cookie 域

认证成功后，SDK 将签名 Token 写入 Cookie（**按根域存储，7 天持久 Cookie + 滑动续期——每次校验成功自动续期，活跃用户不掉登录态，连续 7 天不访问才需重新扫码**；同一浏览器内刷新/重开/开新标签页都不丢登录态）。Cookie 的 `domain` 由**当前页面域名**推导，而不是后端地址（apiBase）：

| 页面域名 | Cookie 落点 | 登录态共享范围 |
|---------|------------|---------------|
| `www.shenzjd.com`（主站） | `.shenzjd.com` | 该域下所有子站共享 |
| `sub.mysite.com`（第三方接入） | `.mysite.com` | 该域下所有子站共享 |
| `localhost` / `127.0.0.1` / IP | 不设 domain | 仅当前页面 |

规则说明：

- **同一注册域内一个认证，全域自动登录**：`sub1.mysite.com` 认证后，`sub2.mysite.com`、根域 `mysite.com` 都能静默通过（凭根域 Cookie）。
- **跨注册域不共享**：`mysite.com` 与 `other.com` 是相互独立的登录态，需各自认证一次（浏览器安全机制，无法绕过）。
- **主站已登录 → 子站有登录态**：`panhub.shenzjd.com` 认证后，访问 `parse.shenzjd.com` 等其他子站同样静默通过。
- **登录态 7 天持久 + 滑动续期（2026-09-19）**：Cookie 带 7 天 `expires`，每次校验成功自动回写续期，活跃用户永不重登；连续 7 天不访问才需重新扫码（2026-09-05~09-18 曾为会话级，更早沿革见主仓库 AGENTS.md「登录态有效期」）。
- **⚠️ 多级公共后缀**：若页面域名属于 `.com.cn` / `.co.uk` 等多级后缀（如 `site.mysite.com.cn`），"取最后两段"会推导出 `.com.cn`，浏览器会拒绝写入。此类接入需确认域名后再接入。

> **为什么 cookie 不跟 apiBase 走？** 早期版本 SDK 曾按 apiBase 域名写 Cookie，导致部署在第三方域名（`apiBase` 指向微信认证后端、页面域名不同）时 Cookie 落错域、后端起总不到凭证、前端反复弹窗（死循环）。SDK 已改为始终按「页面所在域」写 Cookie，与后端地址无关。

## API

### `WxAuth.init(options)`

初始化 SDK。自动检测 Cookie，已登录静默通过，未登录弹出认证窗（小程序扫码）。

```ts
WxAuth.init({
  apiBase?: string,         // 后端地址（可选，默认官方服务）
  required?: boolean,       // 弹窗是否强制（默认 false：带 × 可关闭；true = 不可关闭）
  silent?: boolean,         // 静默初始化（默认 false），true 时不弹窗
  onVerified?: (user) => void,
  onError?: (err) => void,
  onClose?: () => void,     // required=false 时关闭弹窗的回调
});
```

> `siteId` 无需配置：SDK 自动从 `document.referrer` 或当前域名获取并上报。
> `wechatName` / `qrcodeUrl` 随公众号验证码登录一并临时停用（SDK 不再拉取 `/api/sdk/config`）；恢复后仍由后端统一下发，接入方配置无效。

### `WxAuth.requireAuth(options?)`

手动触发认证（用于"登录"按钮、切换账号）。返回 `Promise<boolean>`。

```ts
await WxAuth.requireAuth();                    // 用 init 的 required
await WxAuth.requireAuth({ required: false }); // 本次弹窗可关闭（用户主动点登录）
await WxAuth.requireAuth({ required: true });  // 本次弹窗强制不可关闭（后端 401 拦截）
```

`options.required` 只作用于**本次弹出的弹窗**，不改 init 的全局配置——同一次页面会话里
可以先弹「用户主动登录（可关）」，再弹「后端校验强制登录（不可关）」，互不污染。
`showAuthModal(options?)` 同样接受该入参（两者都是「打开登录弹窗」的入口）。

### `WxAuth.close()`

关闭弹窗。`required=false` 时触发 `onClose` 回调。

### `WxAuth.clearToken()`

清空本地登录凭证（Cookie）。用于"退出登录"按钮的本地清理。

### `WxAuth.revoke()`

服务端注销：吊销当前 Token（加入后端黑名单，任何设备/子域立即失效）+ 清本地凭证。网络失败时自动降级为仅本地清理，不阻塞退出流程。返回 `Promise<boolean>`。

---

## 认证流程

```
用户访问
   │
   ▼
初始化 WxAuth.init()
   │
   ▼
读取 Cookie（7 天持久 + 滑动续期，浏览器内长期有效）
   │
   ├── 有效 ──────────── onVerified()  ✅ 静默通过
   │
   └── 无效 ──→ 显示弹窗（小程序扫码，立即出码）
                    │
                    ▼
            微信扫码打开小程序
                    │
            小程序内「确认登录」
                    │
            网页轮询自动领取 Token
                    │
                    ▼
             保存 Cookie
             onVerified()
```

> 出码失败时停留在扫码面板，提供「刷新二维码」重试入口（原「自动切换到公众号验证码 Tab」降级路径已随该面板临时停用）。

---

## `onVerified(user)` 回调数据

登录成功后回调结构（对齐 `/api/auth/check`）：

```ts
interface AuthUser {
  openid: string;                // 内部统一身份（小程序用户带 mp: 前缀）
  type: 'mp' | 'official';       // 身份类型：小程序 / 公众号
  mpOpenid: string | null;       // 裸小程序 openid（公众号用户为 null），可直接作业务侧 key
  unionid?: string;
  nickname?: string | null;
  headimgurl?: string | null;
  authenticatedAt: string;
}
```

> 同一个人在公众号与小程序登录会得到**两条独立的用户记录**（`type` 不同）。业务侧需要区分或关联时读 `type` / `mpOpenid`；不关心差异的接入方无需任何改动——两种方式签发的 Token 结构完全同构。

---

## 两种模式

**默认是「可选认证」**：用户主动点登录（头像、登录按钮）属于可反悔操作，允许关掉；
「必须登录才能继续」属于业务判断，由调用方显式声明 `required: true`。

| 场景 | 写法 | 形态 |
| --- | --- | --- |
| 用户主动点登录（头像 / 登录按钮） | `requireAuth({ required: false })`（或不传，走 init 默认） | 带 × 可关闭 |
| 后端 401 / 功能前置校验 | `requireAuth({ required: true })` 或 `showAuthModal({ required: true })` | 无 ×，遮罩不可点穿 |

> 前端传入的 `required` 只是 UI 语义，**不构成安全边界**——真正的门禁始终在后端
> （`requireWxAuth` 校验 Token），前端把强制窗改成可关也走不到业务。

### 强制认证 `required: true`（需显式声明）

必须完成认证才能继续，关闭按钮隐藏，点击遮罩无效。

```
  弹窗
 ┌──────────────────────────────────┐
 │  微信登录                         │
 │                                  │
 │     ┌──────────┐                 │
 │     │ 小程序码  │  ← 打开即出码    │
 │     └──────────┘                 │
 │  微信扫描二维码，打开小程序确认登录  │
 └──────────────────────────────────┘
```

### 可选认证 `required: false`（默认）

用户可主动关闭弹窗，关闭时执行 `onClose` 回调。

```
  弹窗
 ┌──────────────────────────────────┐
 │  微信登录                      [×] │
 │  ...                             │
 └──────────────────────────────────┘
    ↓
  用户点击 × 或遮罩
    ↓
  onClose() → 继续浏览受限内容
```

---

## 功能清单

| 功能 | 支持 |
|------|------|
| 小程序扫码登录（打开弹窗立即出码） | ✅ |
| 小程序扫码无需输验证码（扫码 → 小程序内确认 → 网页自动登录） | ✅ |
| 小程序码 5 分钟过期 + 一键刷新 | ✅ |
| 出码失败可在面板内「刷新二维码」重试 | ✅ |
| 有 Cookie 静默认证 | ✅ |
| Cookie 按页面根域写入（跨子域共享） | ✅ |
| 7 天持久 Cookie 登录态 + 滑动续期（浏览器内共享，活跃用户不掉登录） | ✅ |
| 服务抖动/限流重试，不误清凭证、不误弹码框 | ✅ |
| F12 删弹窗自动恢复 | ✅ |
| ~~双 Tab 登录方式（小程序扫码 / 公众号验证码）~~ | ⛔ 2026-09-16 临时停用 |
| ~~出码失败自动回落公众号验证码 Tab~~ | ⛔ 改为面板内重试 |
| ~~验证码输入框（自动跳格 / 粘贴识别 / 键盘导航 / 自动提交）~~ | ⛔ 随公众号登录临时停用 |

> **登录态有效期（2026-09-19）**：web 登录态为 **7 天持久 Cookie + 滑动续期**——每次校验成功自动回写续期，活跃用户永不重登，连续 7 天不访问才需重新扫码；同一浏览器内刷新/重开/开新标签页都不丢登录态；不写 localStorage 备份。2026-09-05~09-18 曾为会话级，更早沿革见主仓库 AGENTS.md。

---

## 平台能力：GitHub（可选）

wx-auth 不止是登录后端，还是**账号系统 + GitHub 能力平台**：用户（微信主账号）可绑定 GitHub，第三方网页领取短命 installation token 后**浏览器直连 GitHub** 操作其仓库内容（图床是第一个能力）。SDK 与站点导航组件会自动带上 GitHub 绑定/安装入口，接入方无需处理任何 GitHub 凭证。

- 状态判定：`GET /api/github/setups`（绑定 / 安装 / 能力就绪度）
- 开通能力：`POST /api/github/setup`（建仓 + 挂仓，幂等）
- 领 token：`POST /api/github/token`（8 小时有效，浏览器直传 GitHub 用）

接入三步（引导 / 开通 / 领 token 直传）的完整话术见主仓库 README「第三方接入话术」章节，或接入方案开环时直接联系我们获取接入说明。

---

## 开发

```bash
npm install
npm run build
```

---

## `silent` 模式：延迟弹窗

默认行为下，`init` 遇到未认证会自动弹窗。如果想**自己控制弹窗时机**（比如免费 3 次搜索后再弹），开启 `silent`：

```ts
// 1. 静默初始化：只校验现有 cookie，不调弹窗
WxAuth.init({
  silent: true,
  required: false,
  onVerified: (user) => { /* 标注已认证 */ },
});

// 2. 业务代码里自由控制弹窗
//    例：免费搜索 3 次后再要求认证
let freeSearches = 3;
async function onSearch() {
  if (freeSearches > 0) {
    freeSearches--;
    doSearch();
  } else {
    const ok = await WxAuth.requireAuth();  // 手动触发弹窗
    if (ok) doSearch();
  }
}
```

| `silent` | `init()` 行为 | 适用场景 |
|----------|--------------|---------|
| `false`（默认） | 需要时自动弹窗 | 付费墙、内测白名单 |
| `true` | 仅校验 cookie，弹窗由 `requireAuth()` 手动触发 | 免费额度、按需解锁 |
