为什么要用双token实现无感刷新

1,223 阅读2分钟

为什么双token,一个token不就行了吗?

只有一个token时,我们称为access_token。因为access_token需要频繁地通过网络请求来进行用户鉴权,所以容易被获取到。这时如果有效期设置得太长,那么access_token被冒用的可能性很大,存在危险。

refresh_token一般长期保存在本地,只会在请求新token的和返回新token的报文中出现,网络中出现的概率低,被获取的概率也就比较低,所以有效期可以设置长一点没事。

综上,因为access_token如果有效期太短,用户就需要频繁地进行身份验证,用户体验差。设置得太长呢,一旦access_token被获取之后被冒用的可能性大。所以使用refresh_token就可以把access_token的有效期缩短,在提高安全性的同时还保证了用户体验。

具体实现

后端

逻辑比较简单直接看代码~

@Post('login')
  login(@Body() userDto: UserDto) {
    const user = users.find((item) => item.username === userDto.username);
    if (!user) {
      throw new BadRequestException('用户不存在');
    }
    if (user.password !== userDto.password) {
      throw new BadRequestException('密码错误');
    }
    const access_token = this.jwtService.sign(
      {
        username: user.username,
        email: user.email,
      },
      {
        expiresIn: '0.5h',
      },
    );
    const refresh_token = this.jwtService.sign(
      {
        username: user.username,
      },
      {
        expiresIn: '7d',
      },
    );
    return {
      userInfo: {
        username: user.username,
        email: user.email,
      },
      access_token,
      refresh_token,
    };
  }
  
  @Get('refresh')
  refresh(@Query('token') token: string) {
    try {
      const data = this.jwtService.verify(token);
      console.log(data);

      const user = users.find((item) => item.username === data.username);
      const access_token = this.jwtService.sign(
        {
          username: user.username,
          email: user.email,
        },
        {
          expiresIn: '0.5h',
        },
      );
      const refresh_token = this.jwtService.sign(
        {
          username: user.username,
        },
        {
          expiresIn: '7d',
        },
      );
      return {
        access_token,
        refresh_token,
      };
    } catch (error) {
      throw new UnauthorizedException('token 失效,请重新登录');
    }
  }

前端

//请求队列数据类型
interface PendingTask {
  config: AxiosRequestConfig;
  resolve: Function;
}
//用于判断当前是否处于刷新状态,避免多次刷新
let refreshing = false;
//定义请求队列
const queue: PendingTask[] = [];

//请求拦截器中自动添加authorization字段用于用户鉴权
axiosInstance.interceptors.request.use(function (config) {
  const accessToken = localStorage.getItem("access_token");
  if (accessToken) {
    config.headers.authorization = "Bearer " + accessToken;
  }
  return config;
});

//基于axios响应拦截器实现无感刷新
axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    let { data, config } = error.response;
    //如果处于刷新状态,则本次请求返回一个promise,并把当前config和resolve加到队列
    if (refreshing) {
      return new Promise((resolve) => {
        queue.push({ config, resolve });
      });
    }
    //如果后端返回401状态码且当前的请求不是刷新token的请求(避免刷新失败了还继续刷新的情况)
    if (data.statusCode === 401 && !config.url.includes("/refresh")) {
      refreshing = true;
      const res = await refreshToken();
      if (res.status === 200) {
        //当刷新成功后,遍历异步请求队列清空所有请求
        queue.forEach(({ config, resolve }) => {
          resolve(axiosInstance(config));
        });
        return axiosInstance(config);
      } else {
        //刷新失败,则说明refresh_token过期,用户需要重新登录
        alert(data || "登录过期,请重新登录");
      }
    } else {
      return error.response;
    }
  }
);

// 刷新token函数
async function refreshToken() {
  const res = await axiosInstance.get("/refresh", {
    params: {
      token: localStorage.getItem("refresh_token"),
    },
  });
  const { access_token, refresh_token } = res.data;
  localStorage.setItem("access_token", access_token);
  localStorage.setItem("refresh_token", refresh_token);
  return res;
}
//用户登录函数
export async function userLogin(username: string, password: string) {
  return await axiosInstance.post("/login", {
    username,
    password,
  });
}