乐趣区

关于jwt:浅析使用-JWT-的正确姿势

浅析应用 JWT 的正确姿态

在很长的一段时间里,我都没有正确的应用 jwt,意识到这个问题之后,把我最实在的思考和总结拿进去和大家分享下,欢送一起探讨

现状

先说下我以前是怎么应用的:

  1. 登录胜利后,将 userId 放进 payload 生成 jwt(有效期 8 小时),而后把 jwt 发送到前端,前端存储下来
  2. 前端每一次拜访 API 需在 header 中携带 jwt
  3. 后端先解析 jwt,拿到 userId 后,数据库查问此用户的权限列表(用户 - 角色 - 权限)
  4. 拿到用户的权限列表后,和以后接口所需的权限进行匹配,匹配胜利返回数据,失败返回 401

jwt 规范

先来查查规范是怎么样的,首先参考了 jwt.io 上面对应用场景的阐明:

  1. Authorization 受权
  2. Information Exchange 信息替换

对于下面的信息,我集体的了解是两个方面:

  1. 容许用户拜访路由、服务和资源,在我这里就是接口所需的权限,也能够 SSO 登录,我这里目前不须要
  2. 能够确定以后用户的身份,在我这里就是 userId 了

优化

当初的用法有以下缺点:

  1. 每次调用接口都须要进行数据库查问权限(用户 - 角色 - 权限),浪费资源
  2. 登录胜利 8 小时后,即便用户始终在不停的应用零碎,但 jwt 还是会生效,须要从新登录

第一点好说,把权限列表也放进 payload,解析结束间接和接口所需权限进行比照。

第二点,把有效期缩短到一个星期,一个月?然而依然会产生正在用着用着 jwt 就生效了,须要从新登录的状况,工夫设置的太长也不平安,因为 jwt 自身就是无状态的,而且权限变更了怎么办,难道要等很久才失效吗,这么看来必须要刷新 jwt 了。

对应的优化点就来了:

  1. 把权限列表放进 payload,不必每次都去数据库查问
  2. 让用户无感的刷新 jwt

刷新 jwt 计划

这里参考了 stackoverflow 下面的探讨:

  1. jwt-refresh-token-flow
  2. JWT (JSON Web Token) automatic prolongation of expiration

而后我确定了我的刷新流程:

  1. 登录胜利后颁发两个 token:accessToken 有效期 1 小时,refreshToken 有效期 1 天
  2. accessToken 生效后返回 401,前端通过 refreshToken 获取新的 accessToken 和新的 refreshToken
  3. refreshToken 生效后返回 403,须要从新登录

也就是说,登录胜利后,在 refreshToken 有效期内,都能够持续操作,并且顺延有效期,再也不会呈现用着用着忽然须要从新登录的状况了。这俩有效期能够自行调整,我这里思考的是 accessToken 最好不能太长,不然调整权限后失效期太短。

后端调整

新增用来刷新 token 的接口,大部分逻辑和登录是一样的,验证 refreshToken 后,返回新的 accessToken 和新的 refreshToken

前端调整

次要的难点在前端局部,前端的刷新逻辑:

  1. 登录胜利后在前端存储 accessToken 和 refreshToken,当前的每一次调用 API 都须要携带 accessToken
  2. 用户一小时后持续操作后端返回 401,此时 accessToken 生效,把这一阶段的所有申请都缓存下来
  3. 应用 refreshToken 获取新的 accessToken 和新的 refreshToken
  4. 应用新的 accessToken 从新发动方才所有缓存下来的申请
  5. 一天之后用户再次操作,后端返回 401,此时 accessToken 生效,把这一阶段的所有申请都缓存下来
  6. 应用 refreshToken 获取,后端返回 403,跳转到登录页从新登录

此处须要思考的是并发申请,须要把 accessToken 生效后期间所有的申请都缓存下来,并且在获取到无效 accessToken 后持续所有未实现的申请

目前我应用的是 axios,应用的拦截器,在此贴出局部外围代码:

// 响应拦截器
axios.interceptors.response.use(
  response => {
    const data = response.data;
    // 没有 code 然而 http 状态为 200 示意内部申请胜利
    if (!data.code && response.status === 200) return data;
    // 依据返回的 code 值来做不同的解决(和后端的公有约定)switch (data.code) {
      case 200:
        return data;
      default:
    }
    // 若不是正确的返回 code,且曾经登录,就抛出谬误
    throw data;
  },
  err => {
    // 这里是返回 http 状态码不为 200 和 304 时候的错误处理
    if (err && err.response) {switch (err.response.status) {
        case 400:
          err.message = '申请谬误';
          break;

        case 401:
          // accesstoken 谬误
          if (router.currentRoute.path === '/login') {break;}
          // 判断是否有 refreshToken
          const root = useRootStore();
          if (!root.refreshToken) {logout();
            break;
          }
          // 进入刷新 token 流程
          // 本次申请的所有配置信息,蕴含了 url、method、data、header 等信息
          const config = err?.config;
          const requestPromise = new Promise(resolve => {addRequestList(() => {
              // 留神这里的 createRequest 函数执行的时候是在 resolve 开始执行的时候,并且返回一个新的 Promise,这个新的 Promise 会代替接口调用的那个
              resolve(createRequest(config));
            });
          });
          refreshTokenRequest();
          // 这里很重要,因为本次申请 401 了,要返回给调用接口的办法返回一个新的申请
          return requestPromise;

        case 403:
          // 403 这里阐明刷新 token 失败,登录曾经到期,须要从新登录
          // 10 秒后革除所有缓存的申请
          setTimeout(() => {clearTempRequestList();
          }, 10000);
          logout();
          break;

        default:
      }
    }
    return Promise.reject(err);
  }
);

刷新局部的逻辑代码:

import axios from 'axios';
import http from './index';
import {useRootStore} from '@/store/root';

// 长期的申请函数列表
const tempRequestList = [];

// 发动刷新 token 的标记位,避免反复刷新申请
let isRefreshing = false;

// 1min 内刷新过 token 标记位
// 为了避免并发的时候,刷新申请结束,tempRequestList 也曾经清空,之后仍有申请返回 403,造成反复刷新
let refreshTokenWithin1Minute = false;

const refreshTokenRequest = () => {if (isRefreshing) {return;}
  if (refreshTokenWithin1Minute) {for (const request of tempRequestList) {request();
    }
    tempRequestList.length = 0;
    return;
  }
  isRefreshing = true;
  refreshTokenWithin1Minute = true;
  const root = useRootStore();
  // 应用刷新 token 申请新的 accesstoken 和刷新 token
  const params = {refreshToken: root.refreshToken};
  http.post('/api/v1/refresh-token', params).then(({data}) => {root.updateAccessToken(data.token);
    root.updateRefreshToken(data.refreshToken);
    root.updateUserId(data.userId);
    for (const request of tempRequestList) {request();
    }
    // 1 min 后革除标记位
    setTimeout(() => {refreshTokenWithin1Minute = false;}, 60000);
    tempRequestList.length = 0;
    isRefreshing = false;
  });
};

const addRequestList = request => {tempRequestList.push(request);
};

const clearTempRequestList = () => {tempRequestList.length = 0;};

const createRequest = config => {
  // 这里必须更新 header 中的 AccessToken
  const root = useRootStore();
  config.headers['Authorization'] = 'Bearer' + root.accessToken;
  return axios(config);
};

export {refreshTokenRequest, createRequest, addRequestList, clearTempRequestList};

源码

前后端有提供源码,能够利用源码调整两个 token 的有效期进行测试

后端局部的源码在这里

前端局部的源码在这里

还有在线的体验地址

退出移动版