【我不熟悉的javascript】02. 使用token和refreshToken的管理用户登录状态
每日鸡汤:但凡你狠心一点,偷偷流泪的人就不是你
目录
前言
一、token 是什么
1. 使用JWT(json web token)
2. 为什么用token验证?
二、使用refreshToken
1. 刷新token
2. 实战,在axios中使用refreshToken
总结
前言
一个简单的带有用户登录系统网页,token用来验证用户,refreshToken则是在用户token过期之后刷新用户token,进而保持登录状态的字段。
一、token 是什么
1. 使用JWT(json web 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信息,在后续的请求中带在http请求的头部,一般是Authorization字段
- token有有效期,而且比较短,过期了需要重新申请
我们随意找一个使用token验证网站的请求来看一下:
在使用token进行验证的时候,需要注意的是:最好是token存在localstorage,而不是cookie中,因为cookie会在每次请求的时候带上,没必要,那我们不如用cookie做验证了
2. 为什么用token验证?
面试必考题,为什么使用token验证,而不是使用cookie和session
题外话,这里面的session,是值得后端的服务中的session,和我们前端的sessionstorage没有半毛钱关系,不要被名字忽悠了,有后端开发经验的同学应该知道,后端可以创建session连接,用来保存当前连接的用户信息等。
所以我们一般所说的cookie验证,其实是cookie/session的缩写,也就是【客户端client / 服务端sever】对应的技术的缩写。客户端使用cookie,服务端使用session,我们前端同学经常会省略掉对服务器端server用的session的描述。 就说我们这个系统用的是cookie验证。
cookie/session的认证方式存在安全的问题
- csrf (暂时不多解释)
- xss攻击
- 浏览器禁用cookie,则无法认证
- session存放用户信息,并且存在服务器上,且不能跨服务器,会增加服务器压力
而使用token验证就没有这些问题
- token认证是无状态的,只需要每次请求携带即可
- 服务器不会存用户的信息,没压力,
- 服务器只会根据下次请求头的字段进行验证,如果客户端没有携带Authorization,那么很简单,就是验证未通过。
二、使用refreshToken
1. 刷新token
基于安全的考虑,token必须设置有效期,假设token 过期时间1天,refreshToken过期时间需要长一点5天,那么今天登录后,明天token就过期了,这个时候需要使用refreshToken请求刷新token的接口,得到新的token和新的refreshToken。这样在用户角度上看,我的网站貌似一直处于登录状态,很方便!!
同时,refreshToken也会过期,如果连refreshToken也过期了,那就没办法了,只能劳烦用户重新登录了,记住就像食品有保质期一样,任何用于验证的token都需要有有效期,只是时间长短的不同。
为了能够请求接口,并且在token过期之后刷新token,我们需要将token和refreshToken都存在本地,一般是localstorage中就可以。
2. 实战,在axios中使用refreshToken
说了一大堆话,我们还是要关心到底怎么用这个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的方法,要求
- 请求接口
- 请求成功,要把新token和新的refreshToken存在本地,供下次请求使用
- 有一个回调函数,供重新请求用,并把新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感觉很关键,然后就是回调函数需要有。