|
|
@@ -15,6 +15,7 @@ import store from '@/store'
|
|
|
import router from '@/router'
|
|
|
import I18n from '@/utils/i18n' // Internationalization 国际化
|
|
|
import Utils from '@/utils/util'
|
|
|
+import setting from '@/setting.js'
|
|
|
// 验权
|
|
|
import { getToken, updateToken, removeRefreshToken } from '@/utils/auth'
|
|
|
import { refreshAccessToken } from '@/api/oauth2/user'
|
|
|
@@ -44,48 +45,76 @@ const service = axios.create({
|
|
|
'Content-Type': 'application/json;charset=utf-8'
|
|
|
}
|
|
|
})
|
|
|
-// 白名单,匿名的URL
|
|
|
-const whiteList = [
|
|
|
- '/oauth2/v3/user/captcha',
|
|
|
- '/oauth2/v3/user/login',
|
|
|
- '/oauth2/v3/user/login/apply',
|
|
|
- '/oauth2/v3/authorize',
|
|
|
- '/oauth2/v3/authorize/apply',
|
|
|
- '/oauth2/v3/authentication',
|
|
|
- '/oauth2/v3/authentication/apply',
|
|
|
- '/oauth2/v3/user/open',
|
|
|
- '/oauth2/v3/user/register',
|
|
|
- '/oauth2/v3/user/reset/passwd',
|
|
|
- '/oauth2/v3/user/send/sms'
|
|
|
-]
|
|
|
+
|
|
|
// 是否正在刷新的标记
|
|
|
let isRefreshing = false
|
|
|
-// 重试队列,每一项将是一个待执行的函数形式
|
|
|
+// 重试队列,每一项是一个待执行的回调函数 (token拉完后会逐个调用)
|
|
|
let requests = []
|
|
|
-// 取消请求
|
|
|
+// 取消请求标志
|
|
|
let cancelRequest = false
|
|
|
// 请求数
|
|
|
let requestCount = 0
|
|
|
+// 临时token请求路径
|
|
|
+const TEMP_TOKEN_API = '/business/v3/short/apply'
|
|
|
+// 缓存用于获取临时token的实例
|
|
|
+let _tempTokenService = null
|
|
|
+// 获取临时token实例
|
|
|
+function getTempTokenService() {
|
|
|
+ if (!_tempTokenService) {
|
|
|
+ _tempTokenService = axios.create({
|
|
|
+ baseURL: BASE_API(),
|
|
|
+ timeout: 10 * 1000,
|
|
|
+ headers: {
|
|
|
+ 'Cache-Control': 'no-cache',
|
|
|
+ 'Content-Type': 'application/json;charset=utf-8'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return _tempTokenService
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取临时token,有效时长5min,仅可使用一次
|
|
|
+ */
|
|
|
+async function fetchTempToken() {
|
|
|
+ const tempTokenService = getTempTokenService()
|
|
|
+ try {
|
|
|
+ const {
|
|
|
+ data: { data = '' }
|
|
|
+ } = (await tempTokenService.post(TEMP_TOKEN_API)) || {}
|
|
|
+ if (data) {
|
|
|
+ return data
|
|
|
+ } else {
|
|
|
+ throw new Error('获取临时 token 失败!')
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('[fetchTempToken] 错误:', err)
|
|
|
+ throw err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 请求(request)拦截器
|
|
|
*
|
|
|
- * get 请求 统一参数放在params里面 // 后台对应只有@RequestParam
|
|
|
- * // `params` 是即将与请求一起发送的 URL 参数
|
|
|
- * // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
|
|
|
- * post 请求 统一参数放在data里面 // json 格式 后台对应@RequestBody ,其他 后台对应@RequestParam
|
|
|
- * === // `data` 是作为请求主体被发送的数据
|
|
|
- * // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
|
|
|
- * // 在没有设置 `transformRequest` 时,必须是以下类型之一:
|
|
|
- * // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
|
|
|
- * // - 浏览器专属:FormData, File, Blob
|
|
|
- * // - Node 专属: Stream
|
|
|
- * ==// post 请求 `params` 这个同get 但要注意 后台对应@RequestParam 请求的`Content-Type`是 application/x-www-form-urlencoded 用 qs.stringify 去构造数据
|
|
|
+ * get请求,统一参数放在params里面,后台对应只有@RequestParam
|
|
|
+ * `params`是即将与请求一起发送的 URL 参数,必须是一个无格式对象(plain object)或 URLSearchParams 对象
|
|
|
+ *
|
|
|
+ * post请求,统一参数放在data里面——json格式,后台对应@RequestBody ,其他 后台对应@RequestParam
|
|
|
+ * `data` 是作为请求主体被发送的数据
|
|
|
+ * 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
|
|
|
+ * 在没有设置 `transformRequest` 时,必须是以下类型之一:
|
|
|
+ * - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
|
|
|
+ * - 浏览器专属:FormData, File, Blob
|
|
|
+ * - Node 专属: Stream
|
|
|
+ * post 请求 `params` 这个同get 但要注意 后台对应@RequestParam 请求的`Content-Type`是 application/x-www-form-urlencoded 用 qs.stringify 去构造数据
|
|
|
*/
|
|
|
service.interceptors.request.use(
|
|
|
async (config) => {
|
|
|
+ // 如果外部已经指定了 baseURL,则不做自动切换
|
|
|
if (!config.baseURL) {
|
|
|
config.baseURL = BASE_API(parseInt(requestCount / 5, 10))
|
|
|
if (MULTIPLE_DOMAIN) {
|
|
|
+ // 轮询多个域名
|
|
|
requestCount >= (API_DOMAIN_NAMES.length - 1) * 5
|
|
|
? (requestCount = 0)
|
|
|
: requestCount++
|
|
|
@@ -102,25 +131,35 @@ service.interceptors.request.use(
|
|
|
if (config.isLoading) {
|
|
|
showFullScreenLoading(config.loading)
|
|
|
}
|
|
|
- // 防止缓存,参数加密
|
|
|
- if (config.method.toUpperCase() === 'GET') {
|
|
|
- if (ENCRYPT_GET_PARAMS) {
|
|
|
- config.params = {
|
|
|
- _p: Utils.isNotEmpty(config.params)
|
|
|
- ? encryptByAes(JSON.stringify(config.params), 'get')
|
|
|
- : undefined,
|
|
|
- _t: Date.parse(new Date()) / 1000
|
|
|
- }
|
|
|
- } else {
|
|
|
- config.params = {
|
|
|
- ...config.params,
|
|
|
- _t: Date.parse(new Date()) / 1000
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // GET 方法做防缓存/参数加密
|
|
|
+ // if (config.method.toUpperCase() === 'GET') {
|
|
|
+ // if (ENCRYPT_GET_PARAMS) {
|
|
|
+ // config.params = {
|
|
|
+ // _p: Utils.isNotEmpty(config.params) ? encryptByAes(JSON.stringify(config.params), 'get') : undefined,
|
|
|
+ // _t: Date.parse(new Date()) / 1000
|
|
|
+ // }
|
|
|
+ // } else {
|
|
|
+ // config.params = {
|
|
|
+ // ...config.params,
|
|
|
+ // _t: Date.parse(new Date()) / 1000
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // }
|
|
|
|
|
|
// 判断是否需要token
|
|
|
- if (whiteList.indexOf(config.url) !== -1) {
|
|
|
+ if (setting.whiteApiList.indexOf(config.url) !== -1) {
|
|
|
+ return config
|
|
|
+ }
|
|
|
+ // 特殊白名单接口,需校验临时token
|
|
|
+ if (setting.whiteApiListWithAuth.indexOf(config.url) !== -1) {
|
|
|
+ try {
|
|
|
+ // 获取临时token
|
|
|
+ const tempToken = await fetchTempToken()
|
|
|
+ // 写入请求头
|
|
|
+ config.headers[HEADER_TOKEN_KEY] = tempToken
|
|
|
+ } catch (err) {
|
|
|
+ console.error('[request] 拉取临时 token 失败,接口:', config.url)
|
|
|
+ }
|
|
|
return config
|
|
|
}
|
|
|
config.headers[HEADER_TOKEN_KEY] = getToken()
|
|
|
@@ -155,23 +194,25 @@ service.interceptors.request.use(
|
|
|
*/
|
|
|
service.interceptors.response.use(
|
|
|
(response) => {
|
|
|
- // console.log(response)
|
|
|
tryHideFullScreenLoading()
|
|
|
- const dataAxios = response.data
|
|
|
- const { state, message, cause } = dataAxios
|
|
|
|
|
|
+ // 下载流直接返回
|
|
|
if (response.config.responseType === 'arraybuffer') {
|
|
|
- // 刷新tonken
|
|
|
return response
|
|
|
}
|
|
|
- // 如果没有 state 代表这不是项目后端开发的接口 比如可能是请求最新版本,或者是请求的数据,或者是
|
|
|
+
|
|
|
+ const dataAxios = response.data
|
|
|
+ const { state, message, cause } = dataAxios
|
|
|
+
|
|
|
+ // 后端接口没有返回约定的state字段,则视为异常
|
|
|
if (state === undefined) {
|
|
|
const msg = '接口异常,没有返回[state]参数\n' + response.config.url
|
|
|
Toast({
|
|
|
message: `${msg}`,
|
|
|
type: 'html',
|
|
|
closeOnClick: true,
|
|
|
- duration: 5 * 1000
|
|
|
+ className: 'custom-toast',
|
|
|
+ duration: 3 * 1000
|
|
|
})
|
|
|
return
|
|
|
}
|
|
|
@@ -184,7 +225,7 @@ service.interceptors.response.use(
|
|
|
) {
|
|
|
return dataAxios
|
|
|
}
|
|
|
- // 处理刷新tonken问题,说明token过期了,刷新token
|
|
|
+ // 处理AccessToken过期,刷新逻辑
|
|
|
if (state === requestState.TOKEN_EXPIRED) {
|
|
|
const config = response.config
|
|
|
if (!isRefreshing) {
|
|
|
@@ -192,24 +233,26 @@ service.interceptors.response.use(
|
|
|
return refreshAccessToken()
|
|
|
.then((res) => {
|
|
|
const data = res.data
|
|
|
- updateToken(data)
|
|
|
- const token = getToken()
|
|
|
- config.headers[HEADER_TOKEN_KEY] = token
|
|
|
- // 已经刷新了token,将所有队列中的请求进行重试
|
|
|
- requests.forEach((cb) => cb(token))
|
|
|
+ updateToken(data) // 更新本地 token
|
|
|
+ const newToken = getToken()
|
|
|
+ config.headers[HEADER_TOKEN_KEY] = newToken
|
|
|
+
|
|
|
+ // 刷新完成后,把队列中的所有请求重新发一次
|
|
|
+ requests.forEach((cb) => cb(newToken))
|
|
|
requests = []
|
|
|
- return service(config)
|
|
|
+ return service(config) // 重试当前请求
|
|
|
})
|
|
|
- .catch((res) => {
|
|
|
- console.error('refreshtoken error =>', res)
|
|
|
+ .catch((err) => {
|
|
|
+ console.error('refreshtoken error =>', err)
|
|
|
removeRefreshToken()
|
|
|
window.location.href = '/'
|
|
|
+ return Promise.reject(err)
|
|
|
})
|
|
|
.finally(() => {
|
|
|
isRefreshing = false
|
|
|
})
|
|
|
} else {
|
|
|
- // 正在刷新token,将返回一个未执行resolve的promise
|
|
|
+ // 如果已经在刷新 token,则把当前请求挂到队列里,等待刷新完成后再触发
|
|
|
return new Promise((resolve) => {
|
|
|
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
|
|
|
requests.push((token) => {
|
|
|
@@ -218,11 +261,11 @@ service.interceptors.response.use(
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
- // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
|
|
|
} else if (
|
|
|
state === requestState.ILLEGAL_TOKEN ||
|
|
|
state === requestState.OTHER_CLIENTS
|
|
|
) {
|
|
|
+ // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
|
|
|
if (!cancelRequest) {
|
|
|
cancelRequest = false
|
|
|
Dialog.confirm({
|
|
|
@@ -235,9 +278,7 @@ service.interceptors.response.use(
|
|
|
.then(() => {
|
|
|
// 终止所有请求
|
|
|
cancelRequest = true
|
|
|
- router.push({
|
|
|
- name: 'login'
|
|
|
- })
|
|
|
+ router.push({ name: 'login' })
|
|
|
})
|
|
|
.catch(() => {
|
|
|
cancelRequest = false
|
|
|
@@ -256,7 +297,6 @@ service.interceptors.response.use(
|
|
|
let errorMsg = ''
|
|
|
if (Utils.isNotEmpty(message)) {
|
|
|
// 有错误消息
|
|
|
- // errorMsg = '错误原因:用户名或密码错误'
|
|
|
errorMsg = Utils.isNotEmpty(dataAxios.cause)
|
|
|
? I18n.t('error.messageCause', {
|
|
|
message,
|
|
|
@@ -267,20 +307,14 @@ service.interceptors.response.use(
|
|
|
})
|
|
|
} else if (Utils.isNotEmpty(cause)) {
|
|
|
// 只有错误原因
|
|
|
- // errorMsg = '错误原因:用户名或密码错误'
|
|
|
-
|
|
|
errorMsg = I18n.t('error.cause', {
|
|
|
cause
|
|
|
})
|
|
|
} else if (I18n.te('error.status.' + state)) {
|
|
|
// 有错误编码
|
|
|
- // errorMsg = '错误原因:用户名或密码错误'
|
|
|
-
|
|
|
errorMsg = I18n.t('error.status.' + state)
|
|
|
} else {
|
|
|
// 未知
|
|
|
- // errorMsg = '错误原因:用户名或密码错误'
|
|
|
-
|
|
|
errorMsg =
|
|
|
message ||
|
|
|
I18n.t('error.unknown', {
|
|
|
@@ -294,7 +328,8 @@ service.interceptors.response.use(
|
|
|
message: `${errorMsg}`,
|
|
|
type: 'html',
|
|
|
closeOnClick: true,
|
|
|
- duration: 5 * 1000
|
|
|
+ className: 'custom-toast',
|
|
|
+ duration: 3 * 1000
|
|
|
})
|
|
|
const err = new Error(errorMsg)
|
|
|
err.state = state
|
|
|
@@ -306,19 +341,25 @@ service.interceptors.response.use(
|
|
|
(error) => {
|
|
|
tryHideFullScreenLoading()
|
|
|
console.error('request-error', error) // for debug
|
|
|
+ console.log(error.response)
|
|
|
if (error && error.response) {
|
|
|
error.message = I18n.t('error.status.' + error.response.status, {
|
|
|
url: error.response.config.url
|
|
|
})
|
|
|
+ error.errMsg =
|
|
|
+ error.response.status +
|
|
|
+ ':' +
|
|
|
+ (error.response.data?.message || error.response.statusText)
|
|
|
} else {
|
|
|
error.state = 500
|
|
|
error.message = I18n.t('error.network') // '服务器君开小差了,请稍后再试'
|
|
|
}
|
|
|
Toast({
|
|
|
- message: error.message || I18n.t('error.network'),
|
|
|
+ message: error.errMsg || error.message || I18n.t('error.network'),
|
|
|
type: 'html',
|
|
|
closeOnClick: true,
|
|
|
- duration: 5 * 1000
|
|
|
+ className: 'custom-toast',
|
|
|
+ duration: 3 * 1000
|
|
|
})
|
|
|
return Promise.reject(error)
|
|
|
}
|