需求场景
我们知道,token 用于保持会话和用户身份认证,它具有时效性。之前的策略是,一旦 token 过期,就强制用户重新登录,如果此时用户正在操作表单,提交时突然被强制登录,那用户体验实在太差了。
正确的策略是,如果该用户是活跃用户的话,当 token 过期时,动态地刷新 token,延长时效。若该用户不是活跃用户,则强制重新登录。
判断活跃用户
那如何判断是否是活跃用户呢?例如,用户在 30 分钟内没有任何操作,则判定为非活跃用户,在 30 分钟内有操作,则判定为活跃用户。当然,这个时间视情况而定。
活跃用户需要在前端进行判断,具体思路是:
在 localStorage 中保存一个 “last_operation_time” 字段,其值为用户最后一次操作的时间。那这个值是怎么来的呢? 监听 mousemove 和 keypress 事件,事件一旦被触发就更新 last_operation_time 的值,让其始终是最后一次操作的时间。
当 token 过期时,用现在的时间减去 last_operation_time,若其差值大于某一规定阈值,怎判定为非活跃用户,否则为活跃用户。
代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | function resetLastTime(): void {const currentTime = new Date().getTime();
 localStorage.setItem("last_operation_time", currentTime.toString());
 }
 
 window.addEventListener("mousemove", resetLastTime, true);
 window.addEventListener("keypress", resetLastTime, true);
 
 
 
 request.interceptors.response.use(res => {
 return res.data;
 }, error => {
 if (error.response.status === 401) {
 const currentTime = new Date().getTime();
 const last_operation_time = localStorage.getItem("last_operation_time");
 if (currentTime - Number(last_operation_time) > 30*60*1000) {
 router.push("/login");
 }
 }
 });
 
 | 
动态刷新 token
动态刷新 token 的思路:
(1)用户登录成功,后台生成两个 token(JWT):access_token 和 refresh_token,一同返回给前端进行保存。
(2)当 access_token 过期时,后台返回 401 错误。前端接收到响应后,判断用户是否为活跃用户,若为非活跃用户,则强制重新登录。否则,请求刷新 access_token 的接口(’/refresh’),并发送 refresh_token。
(3)后台收到请求后,判断 refresh_token 是否已经过期。如果已经过期,返回 401 错误,前端强制重新登录;如果没有过期,那么生成新的 access_token(或者同时生成新的 refresh_token)返回给前端,前端更新 token,并重新发起上一次失败的请求。
单个请求
前端代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | request.interceptors.response.use(res => {return res.data;
 }, async error => {
 if (error.response.status === 401) {
 
 if (error.config.url === "/refresh") {
 router.push("/login");
 } else {
 
 const currentTime = new Date().getTime();
 const last_operation_time = localStorage.getItem("last_operation_time");
 
 if (currentTime - Number(last_operation_time) > 30 * 60 * 1000) {
 router.push("/login");
 localStorage.clear();
 }
 
 else {
 
 const {config} = error;
 
 
 const res: any = await request.post("/refresh", {refresh_token: localStorage.getItem("refresh_token")});
 
 localStorage.setItem("access_token", res.access_token);
 
 config.headers.Authorization = res.access_token;
 return request(config);
 }
 }
 }
 });
 
 | 
多个请求
但是如果在 access_token 过期时同时发起了多个请求,那么就会多次刷新 token,这种情况应该怎么解决呢?
我们通过一个变量 isRefreshing 去控制是否在刷新 token 的状态,如果正在刷新 token,那么之后的请求用一个数组队列保存起来,当 token 刷新完之后,再去逐个重新发起这些请求。
那么如何做到让这些请求处于等待中呢?为了解决这个问题,我们得借助 Promise。将请求存进队列中后,同时返回一个 Promise,让这个 Promise 一直处于 Pending 状态(即不调用 resolve),此时这个请求就会一直等啊等,只要我们不执行 resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用 resolve,逐个重试。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 
 | let isRefreshing: boolean = false;let requestList: ((token: string) => void)[] = [];
 
 
 request.interceptors.response.use(res => {
 return res.data;
 }, async error => {
 if (error.response.status === 401) {
 
 if (error.config.url === "/refresh") {
 router.push("/login");
 } else {
 
 const currentTime = new Date().getTime();
 const last_operation_time = localStorage.getItem("last_operation_time");
 
 if (currentTime - Number(last_operation_time) > 30 * 60 * 1000) {
 router.push("/login");
 localStorage.clear();
 }
 
 else {
 
 if (!isRefreshing) {
 isRefreshing = true;
 const {config} = error;
 
 
 const res: any = await request.post("/refresh", {refresh_token: localStorage.getItem("refresh_token")});
 
 localStorage.setItem("access_token", res.access_token);
 
 requestList.forEach(cb => {
 cb(res.access_token);
 });
 requestList = [];
 isRefreshing = false;
 
 config.headers.Authorization = res.access_token;
 return request(config);
 }
 
 else {
 return new Promise(resolve => {
 requestList.push((token: string) => {
 error.config.headers.Authorization = token;
 resolve(request(error.config));
 });
 });
 }
 }
 }
 }
 });
 
 | 
pyjwt 生成双 token
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | def generate_jwt(payload, expiry, secret=None):"""
 生成jwt
 :param payload: dict 载荷
 :param expiry: datetime 有效期
 :param secret: 密钥
 :return: jwt
 """
 _payload = {'exp': expiry}
 _payload.update(payload)
 
 if not secret:
 secret = current_app.config['JWT_SECRET']
 
 token = jwt.encode(_payload, secret, algorithm='HS256')
 return token.decode()
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | def _generate_tokens(self, user_id, with_refresh_token=True):"""
 生成token 和refresh_token
 :param user_id: 用户id
 :return: token, refresh_token
 """
 
 
 now = datetime.utcnow()
 expiry = now + timedelta(hours=2)
 token = generate_jwt({'user_id': user_id, 'refresh': False}, expiry)
 refresh_token = None
 if with_refresh_token:
 refresh_expiry = now + timedelta(days=current_app.config['JWT_REFRESH_DAYS'])
 refresh_token = generate_jwt({'user_id': user_id, 'refresh': True}, refresh_expiry)
 return token, refresh_token
 
 |