为什么要用双Token无感刷新,它解决了什么问题?

        为了保证安全性,后端设置的Token不可能长期有效,过了一段时间Token就会失效。而发送网络请求的过程又是需要携带Token的,一旦Token失效,用户就要重新登陆,这样用户可能需要频繁登录,体验不好。为了解决这个问题,采取双Token(Access_Token,Refresh_Token)无感刷新,用户完全体会不到Token的变化,但实际上,Token已经刷新了。

如何实现token的无感刷新。

在单点登录的环境下,服务器会给用户一个短时token,而另一个长时刷新token用于换取短时token。通过拦截器和配置更改,可以实现自动刷新token的功能。手动刷新token可以通过修改请求头中的token实现。同时,提到了一种通过excel创建基地址并使用拦截器来实现自动刷新token的方案。

单点登录的模式,并以C型加cookie模式为例详细讲解了用户如何完成登录流程。C型加cookie模式具有很强的控制力,但存在着烧钱、认证中心压力大等问题。为了降低成本和减轻认证中心的压力,出现了Token模式,用户登录后生成一个不能被篡改的字符串作为Token,而不再在服务器表格中记录任何东西。Token模式的优势在于成本低,但控制力较弱。

用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。

标识登录状态的方案有两种:session jwt

session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。

jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。 token 一般是放在一个叫 authorization 的 header 里。

双 token 验证流程
  1. 用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。
  2. 在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。
  3. 客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。
  4. 服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。
  5. 客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。

这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。

session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有

jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

refreshToken.js

import request from './request';
import { getRefreshToken } from './token';
 // 定义一个全局或模块级的变量来存储当前的刷新token请求的Promise
let promise = null;

// 异步函数,用于刷新token
async function refreshToken() {
  // 如果已经有一个刷新token的请求正在进行,直接返回该Promise
  if (promise) {
    return promise;
  }

  // 创建一个新的Promise
  promise = new Promise((resolve, reject) => {
    // 打印日志,表示正在刷新token
    console.log('正在刷新token');

    // 发起GET请求到'/refresh_token'路径,并携带Authorization头部
    request.get('/refresh_token', {
      headers: {
        // 使用getRefreshToken函数获取的当前刷新token
        Authorization: `Bearer ${getRefreshToken()}`,
      },
      // 假设这不是一个标准的请求选项,如果确实需要这样的属性,请确保你的请求库支持它
      // 否则,请移除或替换为正确的请求选项
      // __isRefreshToken: true, // 不规范的属性,通常不建议在请求配置中使用双下划线开头的属性
    })
    .then((resp) => {
      // 如果响应的code为0,表示请求成功,resolve Promise并返回true
      if (resp.code === 0) {
        resolve(true);
      } else {
        // 如果code不为0,可能表示请求失败或token无效,resolve Promise并返回false
        resolve(false);
      }
    })
    .catch((error) => {
      // 如果请求发生错误,reject Promise并传递错误对象
      reject(error);
    });
  });

  // 无论Promise是resolve还是reject,都会执行finally块中的代码
  promise.finally(() => {
    // 将promise设置为null,表示刷新token的请求已经完成
    promise = null;
  });

  // 返回Promise,允许调用者使用await等待其完成
  return promise;
}

request.js

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'

// 创建axios实例
const service = axios.create({
  baseURL: '',// 所有的请求地址前缀部分
  timeout: 25000, // 请求超时时间(毫秒)
  withCredentials: true// 异步请求携带cookie
})

// 请求拦截器
service.interceptors.request.use((config: any) => {
	...
}, error => {
	...
})

// 响应拦截器
service.interceptors.response.use((response: any) => {
	let res = response.data
	if (res.code == '401' && !isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求
		// 刷新token
		try {
			const res = await refreshToken()
			// 保存新的token
			localStorage.setItem('token', res.data.token)
			// 有新token后再重新请求
			response.config.headers.token = localStorage.getItem('token') // 新token
			const resp = await service.request(response.config)
			return resp.data
			// return service(response.config)
		}catch {
			localStorage.clear() // 清除token
			router.replace('/login') // 跳转到登录页
		}
	}
}, error => {
	...
	console.log('error', error)
	return Promise.reject(error)
})

问题一:如何防止多次刷新token

为了防止多次刷新token,可以通过一个变量isRefreshing 去控制是否在刷新token的状态

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'

// 创建axios实例
const service = axios.create({
   baseURL: '',// 所有的请求地址前缀部分
  timeout: 25000, // 请求超时时间(毫秒)
  withCredentials: true// 异步请求携带cookie
})

// 请求拦截器
service.interceptors.request.use((config: any) => {
	...
}, error => {
	...
})

// 响应拦截器
service.interceptors.response.use((response: any) => {
	let res = response.data
	let isRefreshing = false
	if (res.code == '401' && ! isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求
		if (!isRefreshing) {
			isRefreshing = true
			// 刷新token
			try {
				const res = await refreshToken()
				// 保存新的token
				localStorage.setItem('token', res.data.token)
				// 有新token后再重新请求
				response.config.headers.token = localStorage.getItem('token') // 新token
				const resp = await service.request(response.config)
				return resp.data
				// return service(response.config)
			}catch {
				localStorage.clear() // 清除token
				router.replace('/login') // 跳转到登录页
			}
			isRefreshing = false
		}
	}
}, error => {
	...
	console.log('error', error)
	return Promise.reject(error)
})

问题二:同时发起两个或者两个以上的请求时,怎么刷新token

当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。

那么如何做到让这个请求处于等待中呢?

为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'

// 创建axios实例
const service = axios.create({
   baseURL: '',// 所有的请求地址前缀部分
  timeout: 25000, // 请求超时时间(毫秒)
  withCredentials: true// 异步请求携带cookie
})

// 请求拦截器
service.interceptors.request.use((config: any) => {
	...
}, error => {
	...
})

// 响应拦截器
service.interceptors.response.use((response: any) => {
	let res = response.data
	let isRefreshing = false
	let requests = [] // 请求队列
	if (res.code == '401' && isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求
		if (!isRefreshing) {
			isRefreshing = true
			// 刷新token
			try {
				const res = await refreshToken()
				// 保存新的token
				localStorage.setItem('token', res.data.token)
				// 有新token后再重新请求
				response.config.headers.token = localStorage.getItem('token') // 新token
				
				// token 刷新后将数组的方法重新执行
		        requests.forEach((cb) => cb(token))
		        requests = [] // 重新请求完清空
		        
				const resp = await service.request(response.config)
				return resp.data
				// return service(response.config)
			}catch {
				localStorage.clear() // 清除token
				router.replace('/login') // 跳转到登录页
			}
			isRefreshing = false
		} else {
			// 返回未执行 resolve 的 Promise
			return new Promise(resolve => {
				// 用函数形式将 resolve 存入,等待刷新后再执行
				request.push(token => {
					response.config.headers.token = `${token}`
					resolve(service(response.config))
				})
			})
		}
	}
}, error => {
	...
	console.log('error', error)
	return Promise.reject(error)
})

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部