smart_archives_web/src/utils/request.js

352 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken, getUserId, getSecretKey } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/bonus'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import { decryptWithSM4, encryptWithSM4, hashWithSM3AndSalt } from '@/utils/sm'
import { generateRequestSignature } from '@/utils/crypto-js'
const systemConfig = {
requestConfig: {
encryptRequest: process.env.VUE_APP_ENV === 'production' ? true : true,
checkIntegrity: process.env.VUE_APP_ENV === 'production' ? true : true,
encryptResponse: process.env.VUE_APP_ENV === 'production' ? true : true,
},
}
let downloadLoadingInstance
export let isRelogin = { show: false }
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 30000,
})
// 判断是否为二进制数据File/Blob
function isBinaryData(value) {
return (
(typeof File !== 'undefined' && value instanceof File) ||
(typeof Blob !== 'undefined' && value instanceof Blob)
)
}
// request 拦截器
service.interceptors.request.use(
(config) => {
const headers = config.headers || {}
const {
isToken = true,
encryptRequest = process.env.NODE_ENV === 'development' ? true : true,
checkIntegrity = process.env.NODE_ENV === 'development' ? true : true,
encryptResponse = process.env.NODE_ENV === 'development' ? true : true,
repeatSubmit = false,
skipReplayProtection = false
} = headers
// 设置请求头
config.headers['encryptRequest'] = systemConfig.requestConfig.encryptRequest && encryptRequest ? 'true' : 'false'
config.headers['checkIntegrity'] = systemConfig.requestConfig.checkIntegrity && checkIntegrity ? 'true' : 'false'
config.headers['encryptResponse'] = systemConfig.requestConfig.encryptResponse && encryptResponse ? 'true' : 'false'
const isRepeatSubmit = repeatSubmit
// 处理 Token
if (getToken() && isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
// 添加防重放签名头(如果不是跳过重放保护的请求)
if (!skipReplayProtection) {
try {
const userId = getUserId()
const userSecret = getSecretKey()
const method = config.method.toUpperCase()
const timestamp = Date.now().toString()
let requestUrl = config.url
if (config.params && typeof config.params === 'object') {
// 使用URLSearchParams自动处理嵌套对象
const searchParams = new URLSearchParams()
Object.entries(config.params).forEach(([key, value]) => {
if (value === undefined || value === null || value === '' || value === 'undefined') {
return
}
if (typeof value === 'object' && value !== null) {
// 对于对象参数,后端期望的是 params[beginTime] 格式
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
if (nestedValue !== undefined && nestedValue !== null && nestedValue !== '') {
searchParams.append(`${key}[${nestedKey}]`, nestedValue.toString())
}
})
} else {
searchParams.append(key, value.toString())
}
})
const paramsString = searchParams.toString()
if (paramsString) {
requestUrl += '?' + paramsString
}
}
const signature = generateRequestSignature(userId, timestamp, method, requestUrl, userSecret)
config.headers['timestamp'] = timestamp
config.headers['X-Signature'] = signature
} catch (error) {
console.warn('生成防重放签名失败:', error)
}
}
// GET 请求处理 - 统一处理加密逻辑
if (config.method === 'get' && config.params) {
// 如果需要加密 GET 请求
if (systemConfig.requestConfig.encryptRequest && encryptRequest) {
// 将参数转换为查询字符串
let paramsString = tansParams(config.params)
// 移除末尾的 & 字符
if (paramsString.endsWith('&')) {
paramsString = paramsString.slice(0, -1)
}
if (paramsString) {
// 添加完整性校验哈希
const hash = hashWithSM3AndSalt(paramsString)
const encryptedParams = encryptWithSM4(paramsString + '|' + hash)
// 清空原始 params
config.params = {}
// 如果 URL 已经有查询参数,需要先清理
let baseUrl = config.url
const questionMarkIndex = baseUrl.indexOf('?')
if (questionMarkIndex !== -1) {
baseUrl = baseUrl.substring(0, questionMarkIndex)
}
// 设置加密后的查询参数
config.url = baseUrl + '?params=' + encodeURIComponent(encryptedParams)
}else{
// 清空原始 params
config.params = {}
}
} else {
// 不加密的情况,保持原有逻辑
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.params = {}
config.url = url
}
}
// POST/PUT 请求处理
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
let contentType = config.headers['Content-Type']
// 处理 multipart/form-data: 仅加密非二进制字段(如 params忽略文件本身
const isFormData = (typeof FormData !== 'undefined') && (config.data instanceof FormData)
if (isFormData) {
if (systemConfig.requestConfig.encryptRequest && encryptRequest) {
const newForm = new FormData()
// 遍历原始 FormData二进制原样文本字段进行加密
for (const [key, value] of config.data.entries()) {
if (isBinaryData(value)) {
newForm.append(key, value)
} else if (typeof value === 'string') {
const payload = value + '|' + hashWithSM3AndSalt(value)
newForm.append(key, encryptWithSM4(payload))
} else {
// 其他类型(如对象)先转字符串再加密
try {
const stringified = JSON.stringify(value)
const payload = stringified + '|' + hashWithSM3AndSalt(stringified)
newForm.append(key, encryptWithSM4(payload))
} catch (e) {
// 兜底:直接追加原值
newForm.append(key, value)
}
}
}
config.data = newForm
}
// 对于 FormData跳过重复提交体积计算不可可靠 stringify
return config
}
// 非 multipart/form-data 的 JSON 等请求
let data = typeof config.data === 'object' ? JSON.stringify(config.data) : config.data
if (contentType && contentType.includes('application/json') && typeof data !== 'undefined') {
// 加密数据
if (systemConfig.requestConfig.encryptRequest && encryptRequest) {
config.data = encryptWithSM4(data + '|' + hashWithSM3AndSalt(data))
}
}
// 检查请求数据大小(仅针对可 stringify 的场景)
const requestSize = JSON.stringify({
url: config.url,
data: typeof data === 'string' ? data : '[non-string-data]',
time: Date.now(),
}).length
const limitSize = 1000 * 1024 * 1024
if (requestSize >= limitSize) {
console.warn(
`[${config.url}]: 请求数据大小超出允许的5MB限制无法进行防重复提交验证。`,
)
return config
}
// 防止重复提交
const sessionObj = cache.session.getJSON('sessionObj') || {}
const requestObj = { url: config.url, data: data, time: Date.now() }
if (
sessionObj.data === requestObj.data &&
requestObj.time - sessionObj.time < 0 &&
sessionObj.url === requestObj.url
) {
console.warn(`[${sessionObj.url}]: 数据正在处理,请勿重复提交`)
return Promise.reject(new Error('数据正在处理,请勿重复提交'))
}
cache.session.setJSON('sessionObj', requestObj)
}
return config
},
(error) => {
console.error(error)
return Promise.reject(error)
},
)
// 响应拦截器(保持不变)
service.interceptors.response.use(
(res) => {
if (res.headers.encryptresponse && !res.data.hasOwnProperty('code')) {
res.data = JSON.parse(decryptWithSM4(res.data))
}
// 未设置状态码则默认成功状态
const code = res.data.code || 200
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (
res.request.responseType === 'blob' ||
res.request.responseType === 'arraybuffer'
) {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
const currentPath = window.location.pathname
console.log('🚀 ~ currentPath:', currentPath)
// 👉 判断当前是否已经在登录页
if (currentPath === '/login') return // 已经在目标页,不再跳转
isRelogin.show = true
MessageBox.confirm(
'登录状态已过期,您可以继续留在该页面,或者重新登录',
'系统提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
},
)
.then(() => {
isRelogin.show = false
store.dispatch('LogOut').then(() => {
location.href =
process.env.VUE_APP_ENV === 'production'
? '/smart-archiving/index'
: '/index'
})
})
.catch(() => {
isRelogin.show = false
})
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},
(error) => {
let { message } = error
let { response } = error
if (message == 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code')) {
if(response && response.data && response.data.isIp){
// ip 不在白名单中错误提示
message = response.data.msg;
}else{
message = '系统接口' + message.substr(message.length - 3) + '异常'
}
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
},
)
// 通用下载方法(保持不变)
export function download(url, params, filename, config) {
downloadLoadingInstance = Loading.service({
text: '正在下载数据,请稍候',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
})
return service
.post(url, params, {
transformRequest: [
(params) => {
return tansParams(params)
},
],
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
encryptResponse: false,
},
responseType: 'blob',
...config,
})
.then(async (data) => {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg =
errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg)
}
downloadLoadingInstance.close()
})
.catch((r) => {
console.error(r)
Message.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
}
export default service