在开发 vscode 插件以及 electron 等应用时难免都会遇到需要登录的场景,但通常并不是一定要在每个应用中都实现一遍登录流程。特别是当一些子应用附属于一个大的平台应用时,跳转到统一的平台应用去登录授权是合理且安全的(比如可以简化开发流程、集中身份验证、统一权限管理等)。本篇就以 vscode 插件和 electron 应用为例,简单聊聊这里的一些实践。
流程简介
为便于说明,本篇将主要以 vscode 插件跳转 GitHub 进行登录来示例。以下是主要流程:
- 用户点击登录后跳转到 GitHub
- GitHub 要求用户登录,然后询问:“xxx 要求获得账户权限,是否同意?”
- 用户同意,GitHub 则跳转到登录回调地址,并带上授权码
- 插件侧接收到授权码,然后向 GitHub 请求 token
- GitHub 返回 token
- 插件侧根据 token 请求用户数据
了解的同学会发现,这其实就是 OAuth2.0 的授权码模式,是的,它作为一种相对经典且安全的授权方式,被广泛地应用于用户登录、第三方应用授权等场景。这里不多展开,我们先继续后面的登录流程。
实践过程
注册 OAuth 应用
该步骤主要是为了获取 OAuth 登录授权流程中需要的 clent_id 和 clent_secret,以方便后续授权服务验证依赖方应用。点击 Register a new OAuth app 即可注册 GitHub OAuth 应用,以下是填写示例:

我这里填写的主页和登录回调页地址都是一个本地服务地址,实际业务中你也可以填写一个部署在云端的服务地址,都可以,只要你在拿到授权码后能请求到用户信息并同步给客户端就好。另外,经过我的测试,我发现即使真实的回调地址的端口号与这里注册的不一致,也不影响授权,我理解底层应该主要还是校验的 host、clent_id 和 clent_secret 吧。扯远了,,我们继续后面的流程!
跳转登录
当用户点击插件界面的“登录”按钮时,我们需要跳转到 GitHub,跳转的链接如下:
https://github.com/login/oauth/authorize?client_id=xxxx &redirect_uri=http://localhost:53225/login/callback&state=xxxxx&scope=read:user%2520user:email
这里的 client_id 即是上面注册完 OAuth 应用后生成的客户端 id。redirect_uri 是注册时填写的登录回调地址。state 是每次登录时客户端随机生成的一个字符串,登录后,GitHub 会原封不动返回这个参数。scope 表示请求的权限范围,这里我们仅请求用户的基本数据及邮箱地址即可,详细说明见 Scopes for OAuth apps - GitHub Docs。
关于 redirect_uri 所对应的本地服务地址,这里先简单说明下,我是在用户点击登录时,在本地起了一个 http server,以此来处理登录回调后的相关逻辑,但这种方式在 vscode 插件的登录场景下还是有一定局限性的,我们后面再聊。
登录授权
用户在 GitHub 登录授权后,GitHub 便会重定向到我们通过 redirect_uri 指定的回调地址,并带上授权码(code)以及我们上一步传入的 state 参数,完整链接如下:
http://localhost:53225/login/callback?code=34b2a9f03be59c416147&state=yusWEpUdym93XFFyi00hrFEev9XOqZQC
获取 token
当 GitHub 重定向到登录回调地址时,我们上面提到的本地 server 便会收到相关请求。在从 url 中获取到 code、state 后,首先应该判断该 state 的值与跳转登录时生成的 state 的值是否一致,如果不一致则说明是伪造请求,应该立即停止后面的逻辑处理,所以 state 的作用就是帮我们防止 CSRF 攻击。
在校验 state 没问题后,我们就可以根据 code 以及注册 OAuth 应用后拿到的 client_id、client_secret 去请求 token 了,如下示例:
const tokenRes = await Axios.post(
"https://github.com/login/oauth/access_token",
{
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
},
{
headers: {
Accept: "application/json",
},
}
);
const access_token = tokenRes.data.access_token;
一定要注意!!我们这里只是为了演示说明,所以将请求 token 相关的逻辑放在了插件侧。而实际业务中,一定要将该逻辑放在服务端,由服务端去请求 token,否则这里的 client_secret 大概率会泄漏出去!
获取用户信息
在获取到 token 后,便可以使用它来获取用户信息了,示例如下:
const userRes = await Axios.get("https://api.github.com/user", {
headers: {
Accept: "application/json",
Authorization: `token ${access_token}`,
},
});
到这里整个登录流程基本就算介绍完了。其实 OAuth2.0 授权码模式本身是清晰明了的,相信大家也没啥问题,主要就是在把它应用于不同的业务场景时,如何做到用户体验的丝滑、流畅、友好,应该是更值得我们去探究的,而这其中的关键应该就是登录回调了。
登录回调
在上面的示例中我们是起了一个本地 server 作为回调地址。当访问回调地址时,本地 server 除了获取 token 及用户信息,以及同步登录态信息到 webview 中,还应根据不同的情况展示不同的页面信息给用户,比如未授权、state 失效、获取用户信息成功后的操作提示指引等。大多数时候做到这些也就够用了,但实际仍有以下两个不足:
- 用户登录授权结束后没有自动切换回对应的 vscode 窗口。
- 如果用户是在 vscode 网页版使用插件,或者是通过 remote-ssh 等工具连接远程后使用插件,那起本地 server 这种方式就行不通了。
针对第一个问题,其实我们只需在登录回调服务成功获取用户信息后,在返回给用户的页面中打开类似这样的一个链接即可:vscode://your-extension-identifier/login/callback?windowId=xxx。这里的 windowId 即就是用户点击登录时的 vscode 窗口 id 了,你可以通过这种方式获取它:
const loginCallbackUri = await vscode.env.asExternalUri(
vscode.Uri.parse(
`${vscode.env.uriScheme}://${your-extension-identifier}/login/callback`
)
);
console.log(loginCallbackUri.query);
// windowId=xxx
这里的 loginCallbackUri.query 就是 windowId=xxx 了。
针对第二个问题,其实上面也有提到一点,就是我们可以用一个部署在云端的服务地址作为登录回调,大致流程如下:
- 用户点击登录后携带
windowId、clientId以及state等参数跳转到登录授权页 - 登录授权成功后,授权服务重定向到登录回调地址,并带上
windowId、code以及state等 - 云端服务校验
state后请求 token 及用户信息等 - 云端服务重定向到一个云端 web 页面,用于展示登录授权是否成功或者一些其它的操作提示指引等
- 云端 web 页面打开 vscode 插件的外链地址,也就是上面提到的
vscode://your-extension-identifier/login/callback?windowId=xxx&userInfo=xxx - 插件注册一个 URI 处理器,以获取用户信息等。当然,通过这种方式同步用户信息并不是必须的,因为你完全可以采用 sse、ws 或者 socket.io 等方式将用户信息推送到客户端。只是如果结合第五步一起做的话,这种方式应该是最不占资源的。注册 URI 处理器的方法如下:
context.subscriptions.push(
vscode.window.registerUriHandler({
async handleUri(uri: vscode.Uri) {
if (uri.path === "/login/callback") {
const query = new URLSearchParams(uri.query);
const userInfo = query.get("userInfo");
...
}
},
})
);
解决了上面两个问题,vscode 插件的登录流程基本就算完善了。而对于 electron 应用的跳转登录流程,也大体一致,这里我们简单梳理下主要步骤即可:
- 用户点击登录按钮后,渲染进程通知主进程进行登录操作
- 主进程收到消息后,启动本地 server 并跳转到登录授权页
- 登录授权结束后,授权服务重定向到本地 server 地址
- 本地 server 收到请求后获取 token 及用户信息等(获取 token 等逻辑同样建议放在服务端,以防 client secret 泄漏)
- 本地 server 返回登录成功等提示信息
- 主进程将登录态等信息发送给渲染进程,同时调用
mainWindow.show()切换回应用窗口
好了,到这里我想介绍的跳转网页进行登录的流程也就差不多了。但以上主要还是围绕着 OAuth2.0 的授权码模式在展开,下面我们再来简单聊聊其它方案。
其它方案
在上面的 OAuth2.0 的授权码模式中,由于在客户端直接请求 token 容易泄漏 client secret,所以我们应该尽可能地将相关逻辑放在服务端去实现。但如果你觉得这种模式对于你的业务场景有点繁琐,还可以去看看 OAuth2.0 的其它几种模式,比如简化模式。OAuth2.0 的简化模式允许授权服务在用户登录授权后直接返回相应的 token,在这种场景下,应用侧结合 state 做好防止 CSRF 攻击的同时,接口层面也尽可能限制为只允许只读操作,好像也不是不可以。
当然除了 OAuth2.0,你还可以选择以 OpenID Connect 的方式来实现你的登录流程。OpenID Connect 是建立在 OAuth2.0 之上的协议,是 OAuth2.0 的一个身份层扩展,它尤其适合只需登录而不用授权的场景。因为从依赖方的角度来看,它如果要的是授权,那其实是可以不需要知道你是谁的,它只需拿到相应的授权去资源服务器获取它想要的资源即可。但 OpenID Connect 关心的是“你是谁”,是需要明确提供用户身份信息给依赖方应用的。所以从这个角度来讲,其实 OAuth2.0 更适合应用授权访问用户数据,而 OpenID Connect 则更适合来做单点登录、第三方登录等。至于 OpenID Connect 登录的详细流程我们就不展开了,有兴趣的同学自行了解即可。
常见问题
多个 vscode 窗口中的插件实例如何实时同步登录态
日常编程时,我们很可能会在多个 vscode 窗口中使用同一个插件,这时就不可避免会遇到如何实时同步登录态到多个插件实例的问题。这里我们说两个方案,如果你还有其它方案欢迎评论交流。
- 由服务端通过 sse、ws 或者 socket.io 等方式将登录态等信息推送给客户端。这样无论用户是在哪个插件实例中更新了登录态,其它插件实例都能实时同步变化。
- 将需要共享的登录态等信息存储在本地文件中,同时监听该文件的变化。如下:
export function createAndWatchUserInfoFile() {
const context = getExtensionContext();
const storageDir = context.globalStorageUri.fsPath;
// 检测存储目录是否存在,不存在则创建
if (!fs.existsSync(storageDir)) {
fs.mkdirSync(storageDir, { recursive: true });
}
// 检测用户信息文件是否存在,不存在则创建
const userInfoFilePath = getUserInfoFilePath();
if (!fs.existsSync(userInfoFilePath)) {
fs.writeFileSync(userInfoFilePath, JSON.stringify({}));
}
// 监听用户信息文件的变化,同步用户名到 webview 面板
fs.watch(userInfoFilePath, (eventType) => {
if (eventType === "change") {
try {
const data = fs.readFileSync(userInfoFilePath, "utf-8");
const userInfo = JSON.parse(data);
refreshUsername(userInfo?.name);
} catch (error) {
console.error("reading user info file error: ", error);
}
}
});
}
这里我用的是 vscode 为每个插件默认提供的全局状态存储目录 context.globalStorageUri.fsPath ,但需要注意的是,这个叶子目录默认是不存在的,需要插件去创建。之后便是创建及监听共享信息文件,以及响应文件变化的逻辑了。但这种方式也仅限于在同一个终端中共享信息,使用时需注意!
总结
本篇主要以 vscode 插件跳转 GitHub 登录为例,展示了 OAuth2.0 的授权码模式在实际业务中的简单应用,electron 等其它应用也都大同小异。并对登录回调以及 vscode 插件的多实例实时共享登录态信息等问题进行了重点讨论。之后简单扩展介绍了 OAuth2.0 的简化模式以及 OAuth2.0 与 OpenID Connect 的联系与区别,只为抛砖引玉,希望大家能在需要的时候灵活使用!
最后,你可以在 vscode 扩展市场中搜索安装 you-you.to-do-list-demo 来体验 OAuth2.0 的登录授权流程,你也可以通过安装 通义灵码 来体验 OpenID Connect 登录的完整流程(如果我没理解错的话)。就这些,感谢阅读!