每日鸡汤:但凡你狠心一点,偷偷流泪的人就不是你
目录
一个简单的带有用户登录系统网页,token用来验证用户,refreshToken则是在用户token过期之后刷新用户token,进而保持登录状态的字段。
我们常说的token,在我们前端看来,就是后台接口返回给我们的一串base64的字符串,我们只需要使用就可以了。
但是你知道这个字符串代表什么嘛?这就是传说中的JWT ( json web token),使用jwt token,我们经常可以简称为使用token做验证,因为jwt应用广泛,而且安全性比较高。
关于token如何生成,以及各个部分的作用,可以自行学习
JSON Web Token 入门教程 - 阮一峰的网络日志
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html总之我们必须要知道的几点是:
我们随意找一个使用token验证网站的请求来看一下:

在使用token进行验证的时候,需要注意的是:最好是token存在localstorage,而不是cookie中,因为cookie会在每次请求的时候带上,没必要,那我们不如用cookie做验证了
面试必考题,为什么使用token验证,而不是使用cookie和session
题外话,这里面的session,是值得后端的服务中的session,和我们前端的sessionstorage没有半毛钱关系,不要被名字忽悠了,有后端开发经验的同学应该知道,后端可以创建session连接,用来保存当前连接的用户信息等。
所以我们一般所说的cookie验证,其实是cookie/session的缩写,也就是【客户端client / 服务端sever】对应的技术的缩写。客户端使用cookie,服务端使用session,我们前端同学经常会省略掉对服务器端server用的session的描述。 就说我们这个系统用的是cookie验证。
cookie/session的认证方式存在安全的问题
而使用token验证就没有这些问题
基于安全的考虑,token必须设置有效期,假设token 过期时间1天,refreshToken过期时间需要长一点5天,那么今天登录后,明天token就过期了,这个时候需要使用refreshToken请求刷新token的接口,得到新的token和新的refreshToken。这样在用户角度上看,我的网站貌似一直处于登录状态,很方便!!
同时,refreshToken也会过期,如果连refreshToken也过期了,那就没办法了,只能劳烦用户重新登录了,记住就像食品有保质期一样,任何用于验证的token都需要有有效期,只是时间长短的不同。
为了能够请求接口,并且在token过期之后刷新token,我们需要将token和refreshToken都存在本地,一般是localstorage中就可以。
说了一大堆话,我们还是要关心到底怎么用这个refreshToken,首先肯定是不能让用户看见的暗地里进行的操作,给用户一种我一直处于登录状态的“错觉”。
所以我们一般在请求的拦截器中使用,假设我们使用的是axios这个插件,那么在我们封装的请求的时候设置一些拦截器,肯定是在reponse中设置,因为只有我们把请求发出去了,服务器才能给我们判断我们请求header中携带的token是否过期。注意!token是否过期是服务器判断的,如果过期了一半会返回状态码401 。我们就在这个时候拦住它!!行,服务器老兄,刚才给你的token,你说过期了,那我就刷新一下获取个新的,然后再用新的请求。
这个是axios官方给的response拦截器的模版
- // Add a response interceptor
- axios.interceptors.response.use(function (response) {
- // Any status code that lie within the range of 2xx cause this function to trigger
- // Do something with response data
- return response;
- }, function (error) {
- // Any status codes that falls outside the range of 2xx cause this function to trigger
- // Do something with response error
- return Promise.reject(error);
- });
我们只需稍加改造:
- import axios from 'axios';
-
- let authorizing = false;
- // 定义一个请求队列,方便我们刷新后重新请求
- let authQueue = [];
-
- axios.interceptors.response.use(
- function (response) {
- return response;
- },
- function (error) {
- // token 过期 401 肯定是走到error这里的
- const { status } = response;
- if (status !== 401) {
- return Promise.reject(error);
- }
- const token = localstorage.getItem('TOKEN'); // 假设token存在localstorage
- if (token) {
- // 如果正在刷新token就返回
- // 并且把当前这个不是刷新token的请求放入请求队列中,用于后续重新请求
- if (authorizing) {
- return new Promise((resolve, reject) => {
- authQueue.push({
- req: error.raw.config,
- resolve,
- reject,
- });
- });
- }
- // 如果不是正在刷新,并且,获取到本地存储的token,和当前请求头携带的一致,就开始刷新
- if (token === error.raw.config.headers.Authorization && !authorizing) {
- // 这是个异步请求的方法 这个方法后面会实现
- startRefreshToken((newToken) => {
- // 这个刷新的方法有一个回掉函数,参数是新的token,用于重新请求
- authorizing = false; // 刷新完毕,修改状态
- if (newToken) {
- // 开始使用新token,重新请求
- authQueue.forEach((item) => {
- item.req.headers = item.req.headers || {};
- item.req.headers.Authorization = newToken;
- axios.request(item.req).then(item.resolve).catch(item.reject);
- });
- } else {
- // 没有新的token, 也就是说本地存的refreshToken也过期了,队列中的每个请求都抛出错误信息
- authQueue.forEach((item) => {
- item.reject('登录过期', error.raw);
- });
- }
- authQueue.length = 0; // 清空请求队列,没错数组可以通过修改长度清空,这个没有不知道的吧
- });
- return new Promise((resolve, reject) => {
- authQueue.push({
- req: error.raw.config,
- resolve,
- reject,
- });
- });
- }
-
-
- // 如果token 已经更新了,或者 没有正在刷新token ,就重新请求当前的请求
-
- // 这一步的目的是保留headers上面的其他信息, 同时可以用下一句更新header.Authorization
- error.raw.config.headers = error.raw.config.headers || {};
- error.raw.config.headers.Authorization = token; // 这个是已经更新了的token
- return axios.request(error.raw.config); // 重新请求
- }
- return Promise.reject(error);
- }
- );
现在来实现startRefreshToken,刷新token的方法,要求
-
- // 是否正在刷新,如果是,就返回,不要重复刷新了
- let refreshing = false;
- //回调函数的数组,因为请求是源源不断的发的,肯定要都拦截,都要重新请求,所以要暂存一下
- let callbacks = [];
- async function startRefreshToken(callback) {
- if (callback) {
- callbacks.push(callback); // 暂存当前请求的callback
- }
- if(refreshing) {
- return;
- }
- refreshing = true;
- let newToken = '';
- let errMsg = '';
- try {
- // 获取我们存在本地的refreshToken
- const oldRefreshToken = localStorage.getItem('REFRESH_TOKEN');
- if (oldRefreshToken) {
- const { data } = await axios.post(
- 'xxx', // 这个是你们后端的api
- {
- refresh_token: oldRefreshToken,
- },
- {
- headers: {}, // 刷新token的请求headers肯定不要携带Authorization字段
- }
- );
-
- // 这是新返回的token和refreshToken,注意refreshToken也会更新
- const { token, refreshToken } = data
-
- newToken = token; // 给callback调用
-
- // 更新本地的存储的值, 这之后还可以用来修改vuex里的值,反正你有新token了,随你干点啥
- localStorage.setItem('TOKEN', token);
- localStorage.setItem('REFRESH_TOKEN', refreshToken);
- }
- } catch (err) {
- errMsg = err.message;
- console.log(errMsg)
- }
- // 执行所有的回调函数
- callbacks.forEach((callback) => {
- // 如果本地存的refreshToken过期了,到这一步 newToken = ''
- callback(newToken);
- });
-
- // 清空回调函数的数组
- callbacks.length = 0;
- // 重置状态
- refreshing = false;
- }
好了,大功告成!!
刷新token是一个必备技能,一定要学会啊,朋友们!!而且好好用心看一遍,真的一点都不难,主要的关键在于如何重新请求已经发送的请求。
就是那个请求队列authQueue感觉很关键,然后就是回调函数需要有。