Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

双token过期问题 #493

Closed
isekiro opened this issue Mar 28, 2023 · 17 comments
Closed

双token过期问题 #493

isekiro opened this issue Mar 28, 2023 · 17 comments

Comments

@isekiro
Copy link

isekiro commented Mar 28, 2023

假如浏览器一直不关闭,refreshToken过期了以后,平台不会跳转到登录页面,会一直卡在加载左侧菜单栏的页面,只能退出登录重新登录才行,这是bug吗
menu-loading

@xiaoxian521
Copy link
Member

过期你只要请求就会重新刷新

@isekiro
Copy link
Author

isekiro commented Mar 28, 2023

过期你只要请求就会重新刷新

也就是后端要设计成无论refreshToken有没有过期,都可以刷新请求才行?我后端目前是refreshToken过期了就要重新通过login去获取双token @xiaoxian521

@xiaoxian521
Copy link
Member

目前平台是 token 过期了,任何一个请求都会触发刷新 token 接口,当然你也可以自行更改

@isekiro
Copy link
Author

isekiro commented Mar 28, 2023

是这样的,我后端是有jwt验证的,后端也会再次验证一次token,如果token过期会返回401错误,虽然平台会触发刷新token,但是我后端检测到token是过期的,就会返回401,这样平台是拿不到token返回的,按理来说,检测到后端返回401就需要返回登录页的

@isekiro
Copy link
Author

isekiro commented Mar 28, 2023

在签发token时生成两个token,accessToken和refreshToken,前端每次请求时携带accessToken,后端发现accessToken过期时,返回token已过期的结果。前端根据后端状态码判断token是否已经过期,如果过期则携带refreshToken请求刷新token的接口,如果refreshToken没有过期,则后端重新生成accessToken和refreshToken返回给前端,到这里都没有问题,如果refreshToken也过期了,则返回结果要求前端重新登录,我们平台在这一步没有返回登陆页面,一直卡在加载左侧菜单栏。

@xiaoxian521
Copy link
Member

在签发token时生成两个token,accessToken和refreshToken,前端每次请求时携带accessToken,后端发现accessToken过期时,返回token已过期的结果。前端根据后端状态码判断token是否已经过期,如果过期则携带refreshToken请求刷新token的接口,如果refreshToken没有过期,则后端重新生成accessToken和refreshToken返回给前端,到这里都没有问题,如果refreshToken也过期了,则返回结果要求前端重新登录,我们平台在这一步没有返回登陆页面,一直卡在加载左侧菜单栏。

这就是你自己的业务逻辑了 很简单自行处理哈

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

在签发token时生成两个token,accessToken和refreshToken,前端每次请求时携带accessToken,后端发现accessToken过期时,返回token已过期的结果。前端根据后端状态码判断token是否已经过期,如果过期则携带refreshToken请求刷新token的接口,如果refreshToken没有过期,则后端重新生成accessToken和refreshToken返回给前端,到这里都没有问题,如果refreshToken也过期了,则返回结果要求前端重新登录,我们平台在这一步没有返回登陆页面,一直卡在加载左侧菜单栏。

这就是你自己的业务逻辑了 很简单自行处理哈

我是个新手~ 这种情况别人大概是什么样的逻辑,如果可以的话,还要麻烦你给点提示

@xiaoxian521
Copy link
Member

请求拦截 中判断 refreshToken 是否过期,过期调用 logout 方法退出登录

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

请求拦截 中判断 refreshToken 是否过期,过期调用 logout 方法退出登录

我目前用的就是这种方法,但是,会报错:[Vue warn]: Maximum recursive updates exceeded in component <sidebarItem>. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

/** 响应拦截 */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
}
if (PureHttp.initConfig.beforeResponseCallback) {
PureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
message("登录异常", { type: "error" });
useUserStoreHook().logOut();
// location.reload();
return Promise.reject($error);
}
);
}

要加 location.reload(); 强行刷新才正常,但是这样影响体验,所以才来开issue问问~

@xiaoxian521
Copy link
Member

贴代码贴完整哈

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

import Axios, {
  AxiosInstance,
  AxiosRequestConfig,
  CustomParamsSerializer
} from "axios";
import {
  PureHttpError,
  RequestMethods,
  PureHttpResponse,
  PureHttpRequestConfig
} from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
import { message } from "@/utils/message";

// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
  // 请求超时时间
  timeout: 10000,
  headers: {
    Accept: "application/json, text/plain, */*",
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  // 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
  paramsSerializer: {
    serialize: stringify as unknown as CustomParamsSerializer
  }
};

class PureHttp {
  constructor() {
    this.httpInterceptorsRequest();
    this.httpInterceptorsResponse();
  }

  /** token过期后,暂存待执行的请求 */
  private static requests = [];

  /** 防止重复刷新token */
  private static isRefreshing = false;

  /** 初始化配置对象 */
  private static initConfig: PureHttpRequestConfig = {};

  /** 保存当前Axios实例对象 */
  private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);

  /** 重连原始请求 */
  private static retryOriginalRequest(config: PureHttpRequestConfig) {
    return new Promise(resolve => {
      PureHttp.requests.push((token: string) => {
        config.headers["Authorization"] = formatToken(token);
        resolve(config);
      });
    });
  }

  /** 请求拦截 */
  private httpInterceptorsRequest(): void {
    PureHttp.axiosInstance.interceptors.request.use(
      async (config: PureHttpRequestConfig) => {
        // 开启进度条动画
        NProgress.start();
        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
        if (typeof config.beforeRequestCallback === "function") {
          config.beforeRequestCallback(config);
          return config;
        }
        if (PureHttp.initConfig.beforeRequestCallback) {
          PureHttp.initConfig.beforeRequestCallback(config);
          return config;
        }
        /** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
        const whiteList = ["/refreshToken", "/login"];
        return whiteList.some(v => config.url.indexOf(v) > -1)
          ? config
          : new Promise(resolve => {
              const data = getToken();
              if (data) {
                const now = new Date().getTime();
                const expired = parseInt(data.expires) - now <= 0;
                if (expired) {
                  if (!PureHttp.isRefreshing) {
                    PureHttp.isRefreshing = true;
                    // token过期刷新
                    useUserStoreHook()
                      .handRefreshToken({ refreshToken: data.refreshToken })
                      .then(res => {
                        const token = res.data.accessToken;
                        config.headers["Authorization"] = formatToken(token);
                        PureHttp.requests.forEach(cb => cb(token));
                        PureHttp.requests = [];
                      })
                      .finally(() => {
                        PureHttp.isRefreshing = false;
                      });
                  }
                  resolve(PureHttp.retryOriginalRequest(config));
                } else {
                  config.headers["Authorization"] = formatToken(
                    data.accessToken
                  );
                  resolve(config);
                }
              } else {
                resolve(config);
              }
            });
      },
      error => {
        return Promise.reject(error);
      }
    );
  }

  /** 响应拦截 */
  private httpInterceptorsResponse(): void {
    const instance = PureHttp.axiosInstance;
    instance.interceptors.response.use(
      (response: PureHttpResponse) => {
        const $config = response.config;
        // 关闭进度条动画
        NProgress.done();
        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
        if (typeof $config.beforeResponseCallback === "function") {
          $config.beforeResponseCallback(response);
          return response.data;
        }
        if (PureHttp.initConfig.beforeResponseCallback) {
          PureHttp.initConfig.beforeResponseCallback(response);
          return response.data;
        }
        return response.data;
      },
      (error: PureHttpError) => {
        const $error = error;
        $error.isCancelRequest = Axios.isCancel($error);
        // 关闭进度条动画
        NProgress.done();
        // 所有的响应异常 区分来源为取消请求/非取消请求
        message("登录异常", { type: "error" });
        useUserStoreHook().logOut();
        // location.reload();
        return Promise.reject($error);
      }
    );
  }

  /** 通用请求工具函数 */
  public request<T>(
    method: RequestMethods,
    url: string,
    param?: AxiosRequestConfig,
    axiosConfig?: PureHttpRequestConfig
  ): Promise<T> {
    const config = {
      method,
      url,
      ...param,
      ...axiosConfig
    } as PureHttpRequestConfig;

    // 单独处理自定义请求/响应回掉
    return new Promise((resolve, reject) => {
      PureHttp.axiosInstance
        .request(config)
        .then((response: undefined) => {
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    });
  }

  /** 单独抽离的post工具函数 */
  public post<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("post", url, params, config);
  }

  /** 单独抽离的get工具函数 */
  public get<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("get", url, params, config);
  }
}

export const http = new PureHttp();


在src/utils/http/index.ts 下,其他代码我都没改动过,只新增了这3行
message("登录异常", { type: "error" });
useUserStoreHook().logOut();
// location.reload();

@xiaoxian521
Copy link
Member

xiaoxian521 commented Mar 29, 2023

按照你的代码我测试了下没问题
你可以这样模拟下场景

image

image

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

按照你的代码我测试了下没问题 你可以这样模拟下场景

你可以试试,拦截响应加了 logout() 以后,登录完停留在首页 http://localhost:8848/#/welcome ,在这里等超时,按F5刷新,再登录,就会出现。我用非国际精简版,除了用户是从后端返回的外,其他没更改,也会出现这种情况。
image

@isekiro
Copy link
Author

isekiro commented Mar 29, 2023

按照你的代码我测试了下没问题 你可以这样模拟下场景

你可以试试,拦截响应加了 logout() 以后,登录完停留在首页 http://localhost:8848/#/welcome ,在这里等超时,按F5刷新,再登录,就会出现。我用非国际精简版,除了用户是从后端返回的外,其他没更改,也会出现这种情况。 image

我能力有限,目前能定位到,src/layout/components/sidebar/sidebarItem.vue 的大概底151行左右的 hasOneShowingChild 这个函数,if (showingChildren.length === 0) 这里会出现这种递归循环,但是不知道怎么修改

@isekiro
Copy link
Author

isekiro commented Mar 30, 2023

logout() 函数里面 router.push("/login"); 替换成 window.location.href = "/"; 问题解决。

@anerg2046
Copy link

refreshToken过期,那一定会走到用refreshToken换取accessToken

所以只需要在这个请求的结果中进行处理即可。

我是这么改的,在store/modules/user.ts大约57行的位置

/** 刷新`token` */
    async handRefreshToken(data) {
      return new Promise<RefreshTokenResult>((resolve, reject) => {
        refreshTokenApi(data)
          .then(data => {
            if (data?.code === 0) {
              setToken(data.data);
              resolve(data);
            } else {
              message(data.msg, { type: "error" });
              removeToken();
              router.push("/login");
            }
          })
          .catch(error => {
            reject(error);
          });
      });
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants