Sfoglia il codice sorgente

bug-4428 接口未授权访问漏洞处理,为白名单接口增加临时token

cfort 11 mesi fa
parent
commit
7664fa9516
2 ha cambiato i file con 121 aggiunte e 62 eliminazioni
  1. 22 0
      src/setting.js
  2. 99 62
      src/utils/request.js

+ 22 - 0
src/setting.js

@@ -31,6 +31,28 @@ export default {
     '/bpmn/alienRegistration/index',
     '/bpmn/alienRegistration/index',
     '/bpmn/satisfactionV2/index'
     '/bpmn/satisfactionV2/index'
   ],
   ],
+  whiteApiList: [
+    '/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'
+  ],
+  whiteApiListWithAuth: [
+    '/business/v3/employee/signInformation/query',
+    '/business/v3/employee/signInformation/save',
+    '/business/v3/employee/qRcode/query',
+    '/business/v3/employee/registrationOutsiders/save',
+    '/business/v3/employee/registrationOutsiders/query',
+    '/business/v3/employee/satisfaction/save',
+    '/business/v3/employee/satisfaction/getQuestionnaireByQrCodeId'
+  ],
   // 全局key
   // 全局key
   globalKey: 'ibps-app-' + version,
   globalKey: 'ibps-app-' + version,
   /**
   /**

+ 99 - 62
src/utils/request.js

@@ -15,6 +15,7 @@ import store from '@/store'
 import router from '@/router'
 import router from '@/router'
 import I18n from '@/utils/i18n' // Internationalization 国际化
 import I18n from '@/utils/i18n' // Internationalization 国际化
 import Utils from '@/utils/util'
 import Utils from '@/utils/util'
+import setting from '@/setting.js'
 // 验权
 // 验权
 import { getToken, updateToken, removeRefreshToken } from '@/utils/auth'
 import { getToken, updateToken, removeRefreshToken } from '@/utils/auth'
 import { refreshAccessToken } from '@/api/oauth2/user'
 import { refreshAccessToken } from '@/api/oauth2/user'
@@ -37,47 +38,73 @@ const service = axios.create({
     'Content-Type': 'application/json;charset=utf-8'
     '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
 let isRefreshing = false
-// 重试队列,每一项将是一个待执行的函数形式
+// 重试队列,每一项是一个待执行的回调函数 (token拉完后会逐个调用)
 let requests = []
 let requests = []
-// 取消请求
+// 取消请求标志
 let cancelRequest = false
 let cancelRequest = false
 // 请求数
 // 请求数
 let requestCount = 0
 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)拦截器
  * 请求(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 => {
 service.interceptors.request.use(async config => {
+  // 如果外部已经指定了 baseURL,则不做自动切换
   if (!config.baseURL) {
   if (!config.baseURL) {
     config.baseURL = BASE_API(parseInt(requestCount / 5, 10))
     config.baseURL = BASE_API(parseInt(requestCount / 5, 10))
     if (MULTIPLE_DOMAIN) {
     if (MULTIPLE_DOMAIN) {
+      // 轮询多个域名
       requestCount >= ((API_DOMAIN_NAMES.length - 1) * 5) ? requestCount = 0 : requestCount++
       requestCount >= ((API_DOMAIN_NAMES.length - 1) * 5) ? requestCount = 0 : requestCount++
     }
     }
   }
   }
@@ -89,7 +116,7 @@ service.interceptors.request.use(async config => {
   if (config.isLoading) {
   if (config.isLoading) {
     showFullScreenLoading(config.loading)
     showFullScreenLoading(config.loading)
   }
   }
-  // 防止缓存,参数加密
+  // GET 方法做防缓存/参数加密
   if (config.method.toUpperCase() === 'GET') {
   if (config.method.toUpperCase() === 'GET') {
     if (ENCRYPT_GET_PARAMS) {
     if (ENCRYPT_GET_PARAMS) {
       config.params = {
       config.params = {
@@ -105,7 +132,19 @@ service.interceptors.request.use(async config => {
   }
   }
 
 
   // 判断是否需要token
   // 判断是否需要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
     return config
   }
   }
   config.headers[HEADER_TOKEN_KEY] = getToken()
   config.headers[HEADER_TOKEN_KEY] = getToken()
@@ -133,23 +172,25 @@ service.interceptors.request.use(async config => {
  * 响应(respone)拦截器
  * 响应(respone)拦截器
  */
  */
 service.interceptors.response.use(response => {
 service.interceptors.response.use(response => {
-  // console.log(response)
   tryHideFullScreenLoading()
   tryHideFullScreenLoading()
-  const dataAxios = response.data
-  const { state, message, cause } = dataAxios
 
 
+  // 下载流直接返回
   if (response.config.responseType === 'arraybuffer') {
   if (response.config.responseType === 'arraybuffer') {
-    // 刷新tonken
     return response
     return response
   }
   }
-  // 如果没有 state 代表这不是项目后端开发的接口 比如可能是请求最新版本,或者是请求的数据,或者是
+
+  const dataAxios = response.data
+  const { state, message, cause } = dataAxios
+
+  // 后端接口没有返回约定的state字段,则视为异常
   if (state === undefined) {
   if (state === undefined) {
     const msg = '接口异常,没有返回[state]参数\n' + response.config.url
     const msg = '接口异常,没有返回[state]参数\n' + response.config.url
     Toast({
     Toast({
       message: `${msg}`,
       message: `${msg}`,
       type: 'html',
       type: 'html',
       closeOnClick: true,
       closeOnClick: true,
-      duration: 5 * 1000
+      className: 'custom-toast',
+      duration: 3 * 1000
     })
     })
     return
     return
   }
   }
@@ -160,40 +201,41 @@ service.interceptors.response.use(response => {
     state === requestState.WARN) {
     state === requestState.WARN) {
     return dataAxios
     return dataAxios
   }
   }
-  // 处理刷新tonken问题,说明token过期了,刷新token
+  // 处理AccessToken过期,刷新逻辑
   if (state === requestState.TOKEN_EXPIRED) {
   if (state === requestState.TOKEN_EXPIRED) {
     const config = response.config
     const config = response.config
     if (!isRefreshing) {
     if (!isRefreshing) {
       isRefreshing = true
       isRefreshing = true
       return refreshAccessToken().then(res => {
       return refreshAccessToken().then(res => {
         const data = res.data
         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 = []
         requests = []
-        return service(config)
-      }).catch(res => {
-        console.error('refreshtoken error =>', res)
+        return service(config) // 重试当前请求
+      }).catch(err => {
+        console.error('refreshtoken error =>', err)
         removeRefreshToken()
         removeRefreshToken()
         window.location.href = '/'
         window.location.href = '/'
+        return Promise.reject(err)
       }).finally(() => {
       }).finally(() => {
         isRefreshing = false
         isRefreshing = false
       })
       })
     } else {
     } else {
-    // 正在刷新token,将返回一个未执行resolve的promise
-      return new Promise((resolve) => {
-      // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
+      // 如果已经在刷新 token,则把当前请求挂到队列里,等待刷新完成后再触发
+      return new Promise(resolve => {
+        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
         requests.push((token) => {
         requests.push((token) => {
           config.headers[HEADER_TOKEN_KEY] = token
           config.headers[HEADER_TOKEN_KEY] = token
           resolve(service(config))
           resolve(service(config))
         })
         })
       })
       })
     }
     }
+  } else if (state === requestState.ILLEGAL_TOKEN || state === requestState.OTHER_CLIENTS) {
     // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
     // 6020201:非法的token;6020202:其他客户端登录了;6020301:Token 过期了;
-  } else if (state === requestState.ILLEGAL_TOKEN ||
-      state === requestState.OTHER_CLIENTS) {
     if (!cancelRequest) {
     if (!cancelRequest) {
       cancelRequest = false
       cancelRequest = false
       Dialog.confirm({
       Dialog.confirm({
@@ -203,9 +245,7 @@ service.interceptors.response.use(response => {
         store.dispatch('ibps/account/fedLogout').then(() => {
         store.dispatch('ibps/account/fedLogout').then(() => {
           // 终止所有请求
           // 终止所有请求
           cancelRequest = true
           cancelRequest = true
-          router.push({
-            name: 'login'
-          })
+          router.push({ name: 'login' })
         }).catch(() => {
         }).catch(() => {
           cancelRequest = false
           cancelRequest = false
         })
         })
@@ -219,7 +259,6 @@ service.interceptors.response.use(response => {
   } else { // 错误处理
   } else { // 错误处理
     let errorMsg = ''
     let errorMsg = ''
     if (Utils.isNotEmpty(message)) { // 有错误消息
     if (Utils.isNotEmpty(message)) { // 有错误消息
-      // errorMsg = '错误原因:用户名或密码错误'
       errorMsg = Utils.isNotEmpty(dataAxios.cause) ? I18n.t('error.messageCause', {
       errorMsg = Utils.isNotEmpty(dataAxios.cause) ? I18n.t('error.messageCause', {
         message,
         message,
         cause: dataAxios.cause
         cause: dataAxios.cause
@@ -227,18 +266,12 @@ service.interceptors.response.use(response => {
         message
         message
       })
       })
     } else if (Utils.isNotEmpty(cause)) { // 只有错误原因
     } else if (Utils.isNotEmpty(cause)) { // 只有错误原因
-      // errorMsg = '错误原因:用户名或密码错误'
-
       errorMsg = I18n.t('error.cause', {
       errorMsg = I18n.t('error.cause', {
         cause
         cause
       })
       })
     } else if (I18n.te('error.status.' + state)) { // 有错误编码
     } else if (I18n.te('error.status.' + state)) { // 有错误编码
-      // errorMsg = '错误原因:用户名或密码错误'
-
       errorMsg = I18n.t('error.status.' + state)
       errorMsg = I18n.t('error.status.' + state)
     } else { // 未知
     } else { // 未知
-      // errorMsg = '错误原因:用户名或密码错误'
-
       errorMsg = message || I18n.t('error.unknown', {
       errorMsg = message || I18n.t('error.unknown', {
         state
         state
       })
       })
@@ -250,7 +283,8 @@ service.interceptors.response.use(response => {
       message: `${errorMsg}`,
       message: `${errorMsg}`,
       type: 'html',
       type: 'html',
       closeOnClick: true,
       closeOnClick: true,
-      duration: 5 * 1000
+      className: 'custom-toast',
+      duration: 3 * 1000
     })
     })
     const err = new Error(errorMsg)
     const err = new Error(errorMsg)
     err.state = state
     err.state = state
@@ -262,19 +296,22 @@ service.interceptors.response.use(response => {
 error => {
 error => {
   tryHideFullScreenLoading()
   tryHideFullScreenLoading()
   console.error('request-error', error) // for debug
   console.error('request-error', error) // for debug
+  console.log(error.response)
   if (error && error.response) {
   if (error && error.response) {
     error.message = I18n.t('error.status.' + error.response.status, {
     error.message = I18n.t('error.status.' + error.response.status, {
       url: error.response.config.url
       url: error.response.config.url
     })
     })
+    error.errMsg = error.response.status + ':' + (error.response.data?.message || error.response.statusText)
   } else {
   } else {
     error.state = 500
     error.state = 500
     error.message = I18n.t('error.network') // '服务器君开小差了,请稍后再试'
     error.message = I18n.t('error.network') // '服务器君开小差了,请稍后再试'
   }
   }
   Toast({
   Toast({
-    message: error.message || I18n.t('error.network'),
+    message: error.errMsg || error.message || I18n.t('error.network'),
     type: 'html',
     type: 'html',
     closeOnClick: true,
     closeOnClick: true,
-    duration: 5 * 1000
+    className: 'custom-toast',
+    duration: 3 * 1000
   })
   })
   return Promise.reject(error)
   return Promise.reject(error)
 }
 }