vscode 插件与 electron 应用跳转网页进行登录的实践

在开发 vscode 插件以及 electron 等应用时难免都会遇到需要登录的场景,但通常并不是一定要在每个应用中都实现一遍登录流程。特别是当一些子应用附属于一个大的平台应用时,跳转到统一的平台应用去登录授权是合理且安全的(比如可以简化开发流程、集中身份验证、统一权限管理等)。本篇就以 vscode 插件和 electron 应用为例,简单聊聊这里的一些实践。

流程简介

为便于说明,本篇将主要以 vscode 插件跳转 GitHub 进行登录来示例。以下是主要流程:

  1. 用户点击登录后跳转到 GitHub
  2. GitHub 要求用户登录,然后询问:“xxx 要求获得账户权限,是否同意?”
  3. 用户同意,GitHub 则跳转到登录回调地址,并带上授权码
  4. 插件侧接收到授权码,然后向 GitHub 请求 token
  5. GitHub 返回 token
  6. 插件侧根据 token 请求用户数据

了解的同学会发现,这其实就是 OAuth2.0 的授权码模式,是的,它作为一种相对经典且安全的授权方式,被广泛地应用于用户登录、第三方应用授权等场景。这里不多展开,我们先继续后面的登录流程。

实践过程

注册 OAuth 应用

该步骤主要是为了获取 OAuth 登录授权流程中需要的 clent_idclent_secret,以方便后续授权服务验证依赖方应用。点击 Register a new OAuth app 即可注册 GitHub OAuth 应用,以下是填写示例:

我这里填写的主页和登录回调页地址都是一个本地服务地址,实际业务中你也可以填写一个部署在云端的服务地址,都可以,只要你在拿到授权码后能请求到用户信息并同步给客户端就好。另外,经过我的测试,我发现即使真实的回调地址的端口号与这里注册的不一致,也不影响授权,我理解底层应该主要还是校验的 hostclent_idclent_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 中获取到 codestate 后,首先应该判断该 state 的值与跳转登录时生成的 state 的值是否一致,如果不一致则说明是伪造请求,应该立即停止后面的逻辑处理,所以 state 的作用就是帮我们防止 CSRF 攻击。

在校验 state 没问题后,我们就可以根据 code 以及注册 OAuth 应用后拿到的 client_idclient_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 失效、获取用户信息成功后的操作提示指引等。大多数时候做到这些也就够用了,但实际仍有以下两个不足:

  1. 用户登录授权结束后没有自动切换回对应的 vscode 窗口。
  2. 如果用户是在 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 了。

针对第二个问题,其实上面也有提到一点,就是我们可以用一个部署在云端的服务地址作为登录回调,大致流程如下:

  1. 用户点击登录后携带 windowIdclientId 以及 state 等参数跳转到登录授权页
  2. 登录授权成功后,授权服务重定向到登录回调地址,并带上 windowIdcode 以及 state
  3. 云端服务校验 state 后请求 token 及用户信息等
  4. 云端服务重定向到一个云端 web 页面,用于展示登录授权是否成功或者一些其它的操作提示指引等
  5. 云端 web 页面打开 vscode 插件的外链地址,也就是上面提到的 vscode://your-extension-identifier/login/callback?windowId=xxx&userInfo=xxx
  6. 插件注册一个 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 应用的跳转登录流程,也大体一致,这里我们简单梳理下主要步骤即可:

  1. 用户点击登录按钮后,渲染进程通知主进程进行登录操作
  2. 主进程收到消息后,启动本地 server 并跳转到登录授权页
  3. 登录授权结束后,授权服务重定向到本地 server 地址
  4. 本地 server 收到请求后获取 token 及用户信息等(获取 token 等逻辑同样建议放在服务端,以防 client secret 泄漏)
  5. 本地 server 返回登录成功等提示信息
  6. 主进程将登录态等信息发送给渲染进程,同时调用 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 窗口中使用同一个插件,这时就不可避免会遇到如何实时同步登录态到多个插件实例的问题。这里我们说两个方案,如果你还有其它方案欢迎评论交流。

  1. 由服务端通过 sse、ws 或者 socket.io 等方式将登录态等信息推送给客户端。这样无论用户是在哪个插件实例中更新了登录态,其它插件实例都能实时同步变化。
  2. 将需要共享的登录态等信息存储在本地文件中,同时监听该文件的变化。如下:
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 登录的完整流程(如果我没理解错的话)。就这些,感谢阅读!

Demo 源码

to-do-list-demo

相关阅读

vscode webview 插件开发(毛坯篇)

vscode webview 插件开发(精装篇)

vscode webview 插件开发(交付篇)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容