nodejs 前后端分别处理token无感刷新

218 阅读2分钟

无感刷新token流程图

无标题-2023-09-01-0926.png

无感刷新请求逻辑

  1. 请求login接口验证账号密码,获取token和refreshToken
  2. 请求index页面携带token进行请求,
  3. 如果成功正常获取数据
  4. 如果token过期则返回code: 401,msg: token过期
  5. 前端挂起当前index请求的回调
  6. 先请求refresh接口,携带token和refreshToken
  7. 如果refreshToken也过期,则直接失败
  8. 如果refreshToken没有过期,则返回新的token给前端
  9. 前端使用新token重新请求index页面 10.得到index页面结果后执行前面挂起的index请求的回调
    11.整个页面token刷新就结束了

token无感刷新前端代码实现

// pinia 中的代码
  actions: {
    async login(user: LoginUserType) {
      const result = await Login.login(user);
      this.token = result.data.token
      this.refreshToken = result.data.refreshToken
      storage.set('token', result.data.token)
      storage.set('refreshToken', result.data.refreshToken)
    },
    async restToken() {
        const result = await Login.restTokenRequest();
        this.token = result?.data?.token
        storage.set('token', this.token)
      },
    
    
    axiosInstance.interceptors.request.use(
      (config) => {
          config.headers.Authorization = `Bearer ${storage.get('token')}`
          if (config.url === '/loginmodule/login/restToken') {
            config.headers.refreshToken = `Bearer ${storage.get('refreshToken')}`
          }
          return config;
      },
      (err) => {
        ElMessage.error(`请求错误${err}`);
      }
    );
    
   axiosInstance.interceptors.response.use(
      async (response) => {
        const { data, msg, code } = response.data;
        if (code === 200) {
          return response.data;
        } else if (code === 400) {
          const loginStore = useLogin()
          await loginStore.restToken()
          return await this.axiosInstance.request(response.config)
        }
        else if (code === 500) {
          ElMessage.error(msg);
          return;
        } else {
          ElMessage.error("服务器出现了未知错误");
          return;
        }
      },
      (err) => {
        ElMessage.error(`${SERVER_ERR}${err}`);
        return;
      }
    );
    
  // login请求的封装函数
  async restTokenRequest() {
    if (LoginAPI.promise) {
      return LoginAPI.promise
    }
    LoginAPI.promise = new Promise<AxiosResponse<Omit<LoginResponesType, "refreshToken">, any>>(async (resolve, reject) => {
      const result = await LoginAPI.api.restToken()
      resolve(result)
    }).finally(() => {
      LoginAPI.promise = null
    })
    return LoginAPI.promise
  }

token无感刷新后端代码实现

后端接口代码是采用IOC框架的一种书写模式

// TS 装饰器 重构 Koa 路由中的方法装饰器
@Controller("/loginmodule")
class LoginController {
  @post("/login")
  async login(ctx: Context) {
    if (ctx.user && ctx.token) {
      return ctx.success({
        user: ctx.user.username,
        token: ctx.token,
      });
    }
    const body = ctx.request.body as LoginUserType;
    const isUser = await userDao.findLoginUser(body);
    if (isUser && isUser.password === body.password) {
      const token = UserService.generateToken(body.username);
      const refreshToken = UserService.generateRefreshToken(body.username);
      return ctx.success({
        user: body.username,
        token,
        refreshToken,
      });
    }

    ctx.fail("用户名不存在或者密码不正确");
  }

  @post("/login/restToken")
  async restToken(ctx: Context) {
    const token = ctx.header.authorization?.split(" ")[1];
    const refreshToken = (ctx.header.refreshtoken as string)?.split(" ")[1];
    if (token && refreshToken) {
      try {
       const result= UserService.checkToken(token) as JwtPayload;
       return ctx.success({
        username: result.username,
        token: token
       });
      } catch (e: any) {
        if (e.message === "jwt expired") {
            try {
                const resultRefresh = UserService.checkRefreshToken(refreshToken) as JwtPayload;
                const updateToken = UserService.generateToken(resultRefresh.username);
                return ctx.success({
                    username: resultRefresh.username,
                    token: updateToken,
                });
            } catch (e: any) {
                ctx.fail("token非法或者已过期请重新登陆");
            } 
        }
      }
        } else {
            ctx.fail("token非法");
        }
    ctx.fail("token非法");
  }

后端中间件处理

export class AuthorizationCheck {
  static whiteList = [/.*\/login.*/];

  static async check(ctx: Context, next: Koa.Next) {
    const isDisable = AuthorizationCheck.whiteList.some((item) => {
      const regexp = RegExp(item);
      return regexp.test(ctx.request.url);
    });
    if (!isDisable) {
      const token = ctx.header.authorization?.split(" ")[1] || "";
      try {
        const user = UserService.checkToken(token);
        ctx.user = user as JwtPayload;
        ctx.token = token;
        return await next();
      } catch (e: any) {
        if (e.message === "jwt expired") {
          return ctx.success({}, 'token已过期', 400)
        } else {
          return ctx.fail('token不合法')
        }
      }
      

    }
    await next();
  }
}