feat: 集成 VSCode Authentication API 实现用户登录
- 新增 Authentication Provider,登录信息显示在左下角 - 支持浏览器登录并自动回调 - 登录/登出后自动刷新窗口 - 侧边栏根据登录状态显示不同按钮
This commit is contained in:
573
docs/authentication-implementation.md
Normal file
573
docs/authentication-implementation.md
Normal 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
|
||||
- 本地回调使用 HTTP(localhost 不受浏览器限制)
|
||||
- 或使用 `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
|
||||
Reference in New Issue
Block a user