/** * 和服务端进行交互 * 它封装了全局 request拦截器、respone拦截器、统一的错误处理、统一做了超时,baseURL设置等 *
* 作者:hugh zhuang * 邮箱:3378340995@qq.com * 日期:2015-11-02-下午3:29:34 * 版权:广州流辰信息技术有限公司 **/ import axios from 'axios' import { Dialog, Toast } from 'vant' import { encryptByAes } from '@/utils/encrypt' 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' import { showFullScreenLoading, tryHideFullScreenLoading } from './loading' import requestState from '@/constants/state' import { BASE_API, BASE_GATEWAY_API } from '@/api/baseUrl' import { HEADER_TOKEN_KEY, HEADER_SYSTEM_ID, HEADER_TENANT_ID, MULTIPLE_DOMAIN, API_DOMAIN_NAMES, ENCRYPT_GET_PARAMS } from '@/constant' const TIMEOUT = 3000 * 100 // 请求超时(timeout)时间 /** * 创建axios实例 */ const service = axios.create({ timeout: TIMEOUT, // request timeout withCredentials: true, // 跨域安全策略 headers: { 'Cache-Control': 'no-cache', 'Content-Type': 'application/json;charset=utf-8' } }) // 是否正在刷新的标记 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 去构造数据 */ 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++ } } if (config.gateway) { config.baseURL = BASE_GATEWAY_API() } config.isLoading = config.isLoading !== undefined && config.isLoading !== null ? config.isLoading : true if (config.isLoading) { showFullScreenLoading(config.loading) } // 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 (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() // 增加系统id if (store && store.getters.systemid) { config.headers[HEADER_SYSTEM_ID] = store.getters.systemid } // 租户ID if (store && store.getters.tenantid) { config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid } // 租户模式 if (store && store.getters.isTenantOpen) { config.headers[HEADER_TENANT_ID] = store.getters.designTenantid ? store.getters.designTenantid : store.getters.tenantid } return config }, error => { tryHideFullScreenLoading() // Do something with request error Utils.log.error('request' + error) // for debug Promise.reject(error) }) /** * 响应(respone)拦截器 */ service.interceptors.response.use(response => { tryHideFullScreenLoading() // 下载流直接返回 if (response.config.responseType === 'arraybuffer') { return response } 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, className: 'custom-toast', duration: 3 * 1000 }) return } // state为200是正确的请求 或者 验证码问题,或者警告类型的错误 自行处理 if (state === requestState.SUCCESS || state === requestState.UNSUPORT || state === requestState.WARNING || state === requestState.WARN) { return dataAxios } // 处理AccessToken过期,刷新逻辑 if (state === requestState.TOKEN_EXPIRED) { const config = response.config if (!isRefreshing) { isRefreshing = true return refreshAccessToken().then(res => { const data = res.data updateToken(data) // 更新本地 token const newToken = getToken() config.headers[HEADER_TOKEN_KEY] = newToken // 刷新完成后,把队列中的所有请求重新发一次 requests.forEach(cb => cb(newToken)) requests = [] return service(config) // 重试当前请求 }).catch(err => { console.error('refreshtoken error =>', err) removeRefreshToken() window.location.href = '/' return Promise.reject(err) }).finally(() => { isRefreshing = false }) } else { // 如果已经在刷新 token,则把当前请求挂到队列里,等待刷新完成后再触发 return new Promise(resolve => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 requests.push((token) => { config.headers[HEADER_TOKEN_KEY] = token resolve(service(config)) }) }) } } else if (state === requestState.ILLEGAL_TOKEN || state === requestState.OTHER_CLIENTS) { // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了; if (!cancelRequest) { cancelRequest = false Dialog.confirm({ title: I18n.t('error.logout.title'), message: I18n.t('error.logout.message') }).then(() => { store.dispatch('ibps/account/fedLogout').then(() => { // 终止所有请求 cancelRequest = true router.push({ name: 'login' }) }).catch(() => { cancelRequest = false }) }).catch(() => { cancelRequest = false }).finally(() => { cancelRequest = false }) } return Promise.reject(new Error(message)) } else { // 错误处理 let errorMsg = '' if (Utils.isNotEmpty(message)) { // 有错误消息 errorMsg = Utils.isNotEmpty(dataAxios.cause) ? I18n.t('error.messageCause', { message, cause: dataAxios.cause }) : I18n.t('error.message', { message }) } else if (Utils.isNotEmpty(cause)) { // 只有错误原因 errorMsg = I18n.t('error.cause', { cause }) } else if (I18n.te('error.status.' + state)) { // 有错误编码 errorMsg = I18n.t('error.status.' + state) } else { // 未知 errorMsg = message || I18n.t('error.unknown', { state }) } if (response.config.url === '/oauth2/v3/user/login/apply') { errorMsg = '错误原因:用户名或密码错误' } Toast({ message: `${errorMsg}`, type: 'html', closeOnClick: true, className: 'custom-toast', duration: 3 * 1000 }) const err = new Error(errorMsg) err.state = state err.cause = cause return Promise.reject(err) } }, // 异常处理 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.errMsg || error.message || I18n.t('error.network'), type: 'html', closeOnClick: true, className: 'custom-toast', duration: 3 * 1000 }) return Promise.reject(error) } ) export default service