OAuth2.0模式授权下静默登录方案

1,063 阅读4分钟

背景

常规Oauth授权方式,需要从第三方服务器获取授权码,跳转到第三方授权页面后,返回当前页面,用code换取本站登录用token,才真正登录成功。

image.png

这样的登录流程,对H5应用来说,用户每次登录都需要重新登录/授权,打断用户操作流程,降低用户体验。当前目标就是在一定程度上减少这个问题的影响。

分析

很容易想到 缓存token

前端可以缓存用户token和授权码,如果token过期失效,用授权码再次获取token,直到授权码过期需要再次授权。这样可以减少跳转到第三方授权页,如果登录的逻辑不是集中在应用登陆页,可以实现无跳转登录。

这里还会有另外一个问题。在用户打开浏览器的时候,会未知当前token是否有效。由于请求是并发,一个请求的登录失效意味着直到登录成功之前的所有登录都是失效的。如果每个失效的登录都重新登录,登录接口压力会很大,并且也是无效的请求,所以这里也是一个需要解决的问题。

这里我选择的是:对于未登录/未确定登陆状态时,阻塞并行请求成串行

方案

缓存token和授权码

在业务中可能存在情况,业务token有效时间很短,第三方的授权码有效时间很长。以飞书为例,第三方授权码有效期为1个月,普通业务的token有效期可能只有一天。这种情况下如果每次请求失败都需要飞书授权,对用户的打断会过频。

故通过每次需要登录的时候,增发一个请求,通过授权码换取token,可以降低授权页出现频度。用上面的例子,可以降低用户授权频次至1/30.

请求拦截器限制未登录时请求并发

在工程中需要发起请求时增加拦截:

  1. 如果没有成功返回的请求,只允许一个请求成功返回后,才可以发送接下来的请求
  2. 如果网页运行过程某一个请求由于登录状态而失败,对当前请求登录后重试。此时也需要阻塞此后发出的请求,直到请求登陆成功。

image.png

这里的代码不能在inteceptor中做(如果用axios的话),需要将暴露出去的方法包裹一层,来做阻塞性的处理。附一个例子:

function loginChecker(callback) {
  return async function loginWrapper(...param) {
    // 没有登录而且不是第一个请求正在检查,需要阻塞后面的接口
    if (!hasLogin && !isChecking) {
      isChecking = true;

      return new Promise((resolve, reject) => {
        // 之前有缓存的token
        callback
          .apply(axios, param)
          .then((response) => {
            // 请求成功
            if (response.status === 200) {
              // 标志请求成功
              isChecking = false;
              hasLogin = true;
              resolve(response);
              // 这里需要通知其他请求实例,停止等待,继续发送请求
            } else {
              // 请求不成功需要重新登录
              login().then(() => {
                isChecking = false;
                hasLogin = true;

                callback
                  .apply(axios, param)
                  .then((response) => resolve(response))
                  .catch((err) => reject(err));
              });
            }
          })
          .catch((err) => reject(err));
      });
    }
    // 检查登陆有效性的请求正在等待相应,其他请求要等待
    if (isChecking) {
      return new Promise((resolve) => {
      //  这里需要监听检查登陆的请求的状态变更
      }).then(() => {
        return callback.apply(axios, param);
      });
    }
    // 当前登陆成功
    return callback.apply(axios, param);
  };
}

axios.post = loginChecker(axios.post);
axios.get = loginChecker(axios.get);

这里阻塞监听有几种方法:

  1. 发布订阅
  2. 全局变量
  3. 轮询(不推荐)

上面的代码是用发布订阅去做的,虽然这部分被隐去了,还好实现也比较简单,对应部分添加了注释。

结语

这里的目的是跳过每次登录,如果可以协调后端可能会有更好的方式。部分场景对于 Oauth 登录的依赖,只是身份验证,这种场景可以通过内部下发的身份信息和本地缓存来达到免登效果,移动端端内应用可以通过设备指纹更方便的获取设备信息;还有一些场景,后端依赖于三方登录鉴权登录来以调用三方接口,这种情况下,按照三方登录周期来登录是免不了的。