wisetc / practice

Practice conclusion
5 stars 0 forks source link

重新思考前端授权方案 #45

Open wisetc opened 1 year ago

wisetc commented 1 year ago

以前的问题

只通过判断是否有保存 token 来判断是否有授权. 而无判断 token 是否有效的过程. 这样的一个后果很可能是 token 是伪造的, 或者 token 已经过期了. 而且当 token 已经过期, 应用不会得到及时的响应. 应用得知 token 已经过期失效往往是通过后端返回了无权限的状态才知道, 而这时可能应该进入了应用内部, 导致出现界面上的数据缺失和其他异常.

改良的契机

1. check接口

每当应用重新加载(页面刷新)时, 都会请求一个 check 接口, 而这个 check 包含了用户的信息, 如果未授权(或token过期)则不能通过该 check 接口获得用户的信息.

可以通过 check 接口是否能获取到用户的信息来判定用户是否已授权.

2. nuxt 框架的 middlewares

nuxt框架是支持 middleware 的, 而middleware 可以访问到 nuxt context. 这个 context 可以访问到 store, route, redirect 等等的诸多信息. 且 middleware 可以先于页面渲染执行. 则可以通过判定用户信息的状态来查知页面的访问权限.

3. vuex 托管了应用的大部分数据, 几乎能够知晓一切, 且易于访问

用户的信息是保存在 vuex 中的, 故应用是可以随时查找用户是否已授权. 且 vuex 的数据是响应式的, 每当有数据的变化, 凡订阅了该份数据的地方都可以得到通知.

改善后的方案

在页面中应用 middleware 名为 auth, 在 middleware 中定义验证的逻辑和验证未通过的逻辑, 且包含对授权状态的观察. 在 vuex 中导出状态 authenticatedensureCheck.

最终的代码

middleware/auth 的实现

// middleware/auth.ts
export default async function ({ store, redirect, route }) {
  const ensureAuth = (authenticated = store.getters.authenticated) => {
    // If the user is not authenticated
    if (!authenticated) {
      return redirect('/login?' + `redirect_url=${encodeURIComponent(route.fullPath)}`);
    }
  };

  await store.dispatch('ensureCheck');
  ensureAuth();

  // keep auth
  const unwatch = store.watch(
    () => store.getters.authenticated,
    val => {
      ensureAuth(val);

      unwatch();
    }
  );
}

其包含了 ensureCheck 的逻辑和 redirect 的逻辑, 以及 keep auth 的逻辑.

ensureCheck 的实现

// store/index.ts
{
  async ensureCheck({ state }) {
    return new Promise(resolve => {
      if (!state.checked) {
    const checkUnwatch = this.watch(
      () => state.checked,
      val => {
        if (val) {
          console.log('`checked` changed');
          resolve(true);
        }

        checkUnwatch && checkUnwatch();
      }
    );
      } else {
    resolve(true);
      }
    });
  }
}

ensureCheck 是一个 promise(action), 其观察 state.checked 的状态, 若其由 false 改为 true, 则 resolve.

check 接口的调用是在应用初始化的时候, 如下定义

// store/index.ts
{
  async init({ dispatch, commit }, ctx) {
    commit(SET_NUXT_CTX, ctx);

    if (getAccessToken()) {
      await dispatch('profile/check');
    }

    commit(SET_CHECKED, true);
    return true;
  },
}

在系统初始化的时候若能获取到应用的 token, 则调用 check 的逻辑.

authenticated 的导出

// store/index.ts
export const getters: GetterTree<S, any> = {
  authenticated(state, getters, rootState, rootGetters) {
    return rootGetters['profile/userInfo'].__loaded;
  },
};