feat: 集成 VSCode Authentication API 实现用户登录

- 新增 Authentication Provider,登录信息显示在左下角
- 支持浏览器登录并自动回调
- 登录/登出后自动刷新窗口
- 侧边栏根据登录状态显示不同按钮
This commit is contained in:
Roe-xin
2025-12-29 18:25:21 +08:00
parent 4288607ee2
commit 53e91fc5a0
5 changed files with 1104 additions and 5 deletions

View File

@ -0,0 +1,573 @@
# IC Coder 认证系统实现文档
## 概述
本文档详细说明了 IC Coder 插件如何集成 VSCode Authentication API实现用户登录功能并在 VSCode 左下角账户区域显示登录状态。
## 架构设计
### 核心组件
1. **ICCoderAuthenticationProvider** - 认证提供者
2. **VSCode Authentication API** - VSCode 官方认证接口
3. **本地 HTTP 服务器** - 处理登录回调
4. **ICViewProvider** - 侧边栏视图(根据登录状态显示不同按钮)
### 工作流程
```
用户点击登录
调用 vscode.authentication.getSession()
ICCoderAuthenticationProvider.createSession()
启动本地 HTTP 服务器(动态端口)
打开浏览器访问登录页面
用户在网站完成登录
网站重定向到 http://localhost:{port}/callback?token=xxx
本地服务器接收 token
创建 AuthenticationSession
VSCode 左下角显示账户信息
```
## 详细实现
### 1. Authentication Provider 实现
文件:`src/services/icCoderAuthProvider.ts`
#### 1.1 类定义
```typescript
export class ICCoderAuthenticationProvider
implements vscode.AuthenticationProvider
{
private _onDidChangeSessions =
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
public readonly onDidChangeSessions = this._onDidChangeSessions.event;
private _sessions: vscode.AuthenticationSession[] = [];
}
```
**关键点:**
- 实现 `vscode.AuthenticationProvider` 接口
- 使用 `EventEmitter` 通知会话变化
- 在内存中维护会话列表
#### 1.2 核心方法
##### getSessions() - 获取会话列表
```typescript
async getSessions(scopes?: readonly string[]): Promise<readonly vscode.AuthenticationSession[]> {
return this._sessions;
}
```
##### createSession() - 创建会话(登录)
```typescript
async createSession(scopes: readonly string[]): Promise<vscode.AuthenticationSession> {
const token = await this.login();
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: token,
account: {
id: "iccoder-user",
label: "IC Coder 用户",
},
scopes: [...scopes],
};
this._sessions.push(session);
await this.saveSessions();
this._onDidChangeSessions.fire({
added: [session],
removed: [],
changed: [],
});
return session;
}
```
**关键点:**
- 调用 `login()` 方法获取 token
- 创建 `AuthenticationSession` 对象
- 保存到 `globalState`
- 触发 `onDidChangeSessions` 事件通知 VSCode
##### removeSession() - 删除会话(登出)
```typescript
async removeSession(sessionId: string): Promise<void> {
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
if (sessionIndex > -1) {
const session = this._sessions[sessionIndex];
this._sessions.splice(sessionIndex, 1);
await this.saveSessions();
this._onDidChangeSessions.fire({
added: [],
removed: [session],
changed: [],
});
}
}
```
### 2. 本地 HTTP 服务器实现
#### 2.1 动态端口分配
```typescript
server.listen(0, () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 3000;
resolve({ server, port });
});
```
**关键点:**
- 使用端口 `0` 让系统自动分配可用端口
- 避免端口冲突问题
- 支持多个用户同时使用
#### 2.2 回调处理
```typescript
if (url.pathname === "/callback") {
const token = url.searchParams.get("token");
if (token) {
// 返回成功页面
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(this.getSuccessPage(iconBase64));
// 关闭服务器
server.close();
// 返回 token
if ((server as any)._loginResolve) {
(server as any)._loginResolve(token);
}
}
}
```
### 3. package.json 配置
#### 3.1 注册 Authentication Provider
```json
{
"contributes": {
"authentication": [
{
"id": "iccoder",
"label": "IC Coder"
}
]
}
}
```
**关键点:**
- `id` 必须与代码中使用的 ID 一致
- `label` 会显示在 VSCode 账户菜单中
#### 3.2 注册命令
```json
{
"contributes": {
"commands": [
{
"command": "ic-coder.login",
"title": "IC Coder: 登录账户",
"category": "IC Coder"
},
{
"command": "ic-coder.logout",
"title": "IC Coder: 退出登录",
"category": "IC Coder"
}
]
}
}
```
### 4. extension.ts 注册
#### 4.1 注册 Authentication Provider
```typescript
export function activate(context: vscode.ExtensionContext) {
// 注册 Authentication Provider
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(
"iccoder",
"IC Coder",
authProvider
)
);
}
```
#### 4.2 登录命令
```typescript
const loginCommand = vscode.commands.registerCommand(
"ic-coder.login",
async () => {
try {
await vscode.authentication.getSession("iccoder", [], {
createIfNone: true,
});
} catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`);
}
}
);
```
**关键点:**
- `createIfNone: true` 会在没有会话时自动调用 `createSession()`
- VSCode 会自动处理 UI 交互
#### 4.3 登出命令
```typescript
const logoutCommand = vscode.commands.registerCommand(
"ic-coder.logout",
async () => {
try {
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (session) {
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
forceNewSession: true,
});
vscode.window.showInformationMessage("已退出登录");
}
} catch (error) {
vscode.window.showInformationMessage("当前未登录");
}
}
);
```
### 5. ICViewProvider 集成
#### 5.1 检查登录状态
```typescript
private async checkLoginStatus(): Promise<boolean> {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
return !!session;
} catch (error) {
return false;
}
}
```
#### 5.2 根据登录状态显示不同按钮
```typescript
resolveWebviewView(webviewView: vscode.WebviewView) {
this.checkLoginStatus().then((isLoggedIn) => {
webviewView.webview.html = this.getWebviewContent(
webviewView.webview,
isLoggedIn
);
});
}
```
```typescript
${isLoggedIn
? '<button class="btn" onclick="openChat()">开始创作</button>'
: '<button class="btn" onclick="login()">登录账户</button>'
}
```
### 6. 网站前端配置
#### 6.1 检测插件登录请求
```javascript
// 在登录页面检测 redirect_uri 参数
const urlParams = new URLSearchParams(window.location.search);
const redirectUri = urlParams.get("redirect_uri");
if (redirectUri) {
// 保存回调地址
localStorage.setItem("plugin_redirect_uri", redirectUri);
}
```
#### 6.2 登录成功后重定向
```javascript
// 用户登录成功,拿到 token
const token = response.data.token;
// 检查是否需要重定向回插件
const redirectUri = localStorage.getItem("plugin_redirect_uri");
if (redirectUri) {
// 重定向回插件,带上 token
window.location.href = `${redirectUri}?token=${token}`;
localStorage.removeItem("plugin_redirect_uri");
} else {
// 正常登录流程
router.push("/dashboard");
}
```
## 关键技术点
### 1. 动态端口分配
**问题:** 固定端口可能被占用,导致登录失败
**解决方案:** 使用端口 `0` 让系统自动分配可用端口
```typescript
server.listen(0, () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 3000;
});
```
### 2. Promise 异步等待
**问题:** 需要等待浏览器登录完成后才能继续
**解决方案:** 使用 Promise 包装回调逻辑
```typescript
return new Promise((resolve, reject) => {
(server as any)._loginResolve = resolve;
(server as any)._loginReject = reject;
});
```
### 3. 会话持久化
**问题:** 重启 VSCode 后需要重新登录
**解决方案:** 使用 `globalState` 保存会话
```typescript
await this.context.globalState.update("icCoderSessions", this._sessions);
```
### 4. 事件通知机制
**问题:** VSCode 需要知道会话状态变化
**解决方案:** 使用 `EventEmitter` 触发事件
```typescript
this._onDidChangeSessions.fire({
added: [session],
removed: [],
changed: [],
});
```
## 用户体验
### 登录流程
1. 用户点击侧边栏"登录账户"按钮
2. 浏览器自动打开登录页面
3. 用户在网站完成登录
4. 浏览器自动跳转到成功页面
5. VSCode 左下角显示"IC Coder 用户"
6. 侧边栏按钮变为"开始创作"
### 登出流程
1. 点击 VSCode 左下角账户图标
2. 选择"IC Coder"账户
3. 点击"退出"按钮
4. 或使用命令 `IC Coder: 退出登录`
## 常见问题
### Q1: 为什么不直接使用 globalState 存储 token
**A:** 使用 VSCode Authentication API 的优势:
- ✅ 统一的用户体验(左下角账户区域)
- ✅ VSCode 自动管理会话生命周期
- ✅ 支持多账户切换
- ✅ 更好的安全性VSCode 负责加密存储)
### Q2: 如何处理 token 过期?
**A:** 可以在 API 请求失败时:
1. 检测 401 错误
2. 调用 `removeSession()` 清除过期会话
3. 提示用户重新登录
### Q3: 如何支持多个账户?
**A:** 修改 `account` 对象:
```typescript
account: {
id: userInfo.id,
label: userInfo.username,
}
```
### Q4: 登录页面如何获取用户信息?
**A:** 可以在登录成功后,通过 API 获取用户信息:
```typescript
const userInfo = await fetch("https://api.iccoder.com/user/info", {
headers: { Authorization: `Bearer ${token}` },
});
const session: vscode.AuthenticationSession = {
account: {
id: userInfo.id,
label: userInfo.username,
},
// ...
};
```
## 安全考虑
### 1. Token 存储
- ✅ 使用 VSCode `globalState` 加密存储
- ✅ 不在代码中硬编码敏感信息
- ✅ Token 仅在内存和加密存储中传递
### 2. 本地服务器
- ✅ 仅监听 `localhost`,不暴露到外网
- ✅ 使用动态端口,避免固定端口被劫持
- ✅ 接收到 token 后立即关闭服务器
- ✅ 设置 5 分钟超时,防止服务器长期运行
### 3. HTTPS 考虑
**当前实现:** 使用 HTTP 本地回调
**生产环境建议:**
- 网站使用 HTTPS
- 本地回调使用 HTTPlocalhost 不受浏览器限制)
- 或使用 `vscode://` 协议(需要网站支持)
## 测试指南
### 1. 本地测试
```bash
# 启动调试模式
按 F5
# 测试登录
1. 打开侧边栏
2. 点击"登录账户"
3. 在浏览器完成登录
4. 检查左下角是否显示账户
# 测试登出
1. 点击左下角账户
2. 选择"IC Coder"
3. 点击"退出"
```
### 2. 调试技巧
```typescript
// 在 ICCoderAuthenticationProvider 中添加日志
console.log("🔐 创建会话:", session);
console.log("🔑 Token:", token);
// 在 ICViewProvider 中添加日志
console.log("🔍 登录状态:", isLoggedIn);
```
### 3. 常见错误排查
| 错误 | 原因 | 解决方案 |
| ------------------------------- | --------------- | ---------------------------------- |
| `getSessions is not a function` | VSCode 版本过低 | 升级到 1.63.0+ |
| 端口被占用 | 固定端口冲突 | 使用动态端口(已实现) |
| 登录后未显示账户 | 未触发事件 | 检查 `_onDidChangeSessions.fire()` |
| 重启后需要重新登录 | 未保存会话 | 检查 `saveSessions()` 调用 |
## 文件结构
```
ic-coder/
├── src/
│ ├── services/
│ │ └── icCoderAuthProvider.ts # Authentication Provider 实现
│ ├── views/
│ │ └── ICViewProvider.ts # 侧边栏视图(集成登录状态)
│ └── extension.ts # 注册 Provider 和命令
├── package.json # 配置 authentication 和 commands
└── docs/
└── authentication-implementation.md # 本文档
```
## 参考资料
- [VSCode Authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication)
- [Authentication Provider Sample](https://github.com/microsoft/vscode-extension-samples/tree/main/authentication-sample)
- [VSCode Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
## 总结
本实现通过以下步骤完成了 VSCode Authentication API 的集成:
1. ✅ 创建 `ICCoderAuthenticationProvider` 类实现认证逻辑
2. ✅ 在 `package.json` 中注册 authentication provider
3. ✅ 在 `extension.ts` 中注册 provider 和命令
4. ✅ 实现本地 HTTP 服务器处理登录回调
5. ✅ 使用动态端口避免冲突
6. ✅ 集成到侧边栏视图,根据登录状态显示不同按钮
7. ✅ 配置网站前端支持插件登录重定向
**最终效果:**
- 用户登录后VSCode 左下角显示"IC Coder 用户"
- 侧边栏根据登录状态显示"登录账户"或"开始创作"按钮
- 支持通过账户菜单或命令进行登录/登出操作
---
**文档版本:** 1.0
**最后更新:** 2025-12-29
**作者:** Roe-xin