diff --git a/.env.development b/.env.development index 302ecd1..8cee688 100644 --- a/.env.development +++ b/.env.development @@ -1,10 +1,10 @@ # 页面标题 -VUE_APP_TITLE = 若依管理系统 +VUE_APP_TITLE = 工程档案资料移交管理系统 # 开发环境配置 ENV = 'development' -# 若依管理系统/开发环境 +# 工程档案资料移交管理系统/开发环境 VUE_APP_BASE_API = '/dev-api' # 路由懒加载 diff --git a/.env.production b/.env.production index b4893b0..ce5b3b8 100644 --- a/.env.production +++ b/.env.production @@ -1,8 +1,8 @@ # 页面标题 -VUE_APP_TITLE = 若依管理系统 +VUE_APP_TITLE = 工程档案资料移交管理系统 # 生产环境配置 ENV = 'production' -# 若依管理系统/生产环境 +# 工程档案资料移交管理系统/生产环境 VUE_APP_BASE_API = '/prod-api' diff --git a/.env.staging b/.env.staging index 209b64e..ebd607b 100644 --- a/.env.staging +++ b/.env.staging @@ -1,5 +1,5 @@ # 页面标题 -VUE_APP_TITLE = 若依管理系统 +VUE_APP_TITLE = 工程档案资料移交管理系统 BABEL_ENV = production @@ -8,5 +8,5 @@ NODE_ENV = production # 测试环境配置 ENV = 'staging' -# 若依管理系统/测试环境 +# 工程档案资料移交管理系统/测试环境 VUE_APP_BASE_API = '/stage-api' diff --git a/.gitignore b/.gitignore index 78a752d..56449a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store node_modules/ +.history/ dist/ npm-debug.log* yarn-debug.log* diff --git a/package.json b/package.json index ab3bdab..16cc4e7 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "ruoyi", "version": "3.9.0", - "description": "若依管理系统", - "author": "若依", + "description": "工程档案资料移交管理系统", + "author": "bonus", "license": "MIT", "scripts": { "dev": "vue-cli-service serve", @@ -39,6 +39,7 @@ "nprogress": "0.2.0", "quill": "2.0.2", "screenfull": "5.0.2", + "sm-crypto": "^0.3.13", "sortablejs": "1.10.2", "splitpanes": "2.4.1", "vue": "2.6.12", diff --git a/src/api/system/user.js b/src/api/system/user.js index b5e3edd..425c070 100644 --- a/src/api/system/user.js +++ b/src/api/system/user.js @@ -1,5 +1,5 @@ import request from '@/utils/request' -import { parseStrEmpty } from "@/utils/ruoyi"; +import { parseStrEmpty } from "@/utils/bonus"; // 查询用户列表 export function listUser(query) { diff --git a/src/main.js b/src/main.js index da01750..897192a 100644 --- a/src/main.js +++ b/src/main.js @@ -18,7 +18,7 @@ import './assets/icons' // icon import './permission' // permission control import { getDicts } from "@/api/system/dict/data" import { getConfigKey } from "@/api/system/config" -import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi" +import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/bonus" // 分页组件 import Pagination from "@/components/Pagination" // 自定义表格工具组件 diff --git a/src/plugins/download.js b/src/plugins/download.js index 8dc83ea..536910a 100644 --- a/src/plugins/download.js +++ b/src/plugins/download.js @@ -3,7 +3,7 @@ import {Loading, Message} from 'element-ui' import { saveAs } from 'file-saver' import { getToken } from '@/utils/auth' import errorCode from '@/utils/errorCode' -import { blobValidate } from "@/utils/ruoyi" +import { blobValidate } from "@/utils/bonus" const baseURL = process.env.VUE_APP_BASE_API let downloadLoadingInstance diff --git a/src/utils/ruoyi.js b/src/utils/bonus.js similarity index 100% rename from src/utils/ruoyi.js rename to src/utils/bonus.js diff --git a/src/utils/configure.js b/src/utils/configure.js new file mode 100644 index 0000000..6e26ee8 --- /dev/null +++ b/src/utils/configure.js @@ -0,0 +1,23 @@ +// SM 配置 +export const SM_CONFIG = { + SALT: '2cc0c5f9f1749f1632efa9f63e902323', // SM3 盐值(16 字节) + SM4_KEY:"78d1295afa99449b99d6f83820e6965c", // SM4 对称加密密钥 + SM4_SALT:"f555adf6c01d0ab0761e626a2dae34a2", + SM2_PUBLIC_KEY: 'your-public-key', // SM2 公钥 + SM2_PRIVATE_KEY: 'your-private-key' // SM2 私钥 +} +// AES 配置 +export const AES_CONFIG = { + AES_KEY: 'zhgd@bonus@zhgd@bonus@1234567890', // AES key值 + AES_IV: '1234567812345678' // AES 偏移量 +} + +export function generateUUID() { + // 使用当前时间戳和随机数生成一个 UUID + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; // 生成随机数 + const v = c === 'x' ? r : (r & 0x3 | 0x8); // 根据 UUID 规范生成相应的值 + return v.toString(16); // 转换为十六进制 + }); +} + diff --git a/src/utils/dict/Dict.js b/src/utils/dict/Dict.js index 994f6f7..b18cb19 100644 --- a/src/utils/dict/Dict.js +++ b/src/utils/dict/Dict.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import { mergeRecursive } from "@/utils/ruoyi" +import { mergeRecursive } from "@/utils/bonus" import DictMeta from './DictMeta' import DictData from './DictData' diff --git a/src/utils/dict/DictMeta.js b/src/utils/dict/DictMeta.js index 617cc00..a631923 100644 --- a/src/utils/dict/DictMeta.js +++ b/src/utils/dict/DictMeta.js @@ -1,4 +1,4 @@ -import { mergeRecursive } from "@/utils/ruoyi" +import { mergeRecursive } from "@/utils/bonus" import DictOptions from './DictOptions' /** diff --git a/src/utils/dict/DictOptions.js b/src/utils/dict/DictOptions.js index 5fd1425..5a226fc 100644 --- a/src/utils/dict/DictOptions.js +++ b/src/utils/dict/DictOptions.js @@ -1,4 +1,4 @@ -import { mergeRecursive } from "@/utils/ruoyi" +import { mergeRecursive } from "@/utils/bonus" import dictConverter from './DictConverter' export const options = { diff --git a/src/utils/index.js b/src/utils/index.js index 9375db7..7234613 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,4 +1,4 @@ -import { parseTime } from './ruoyi' +import { parseTime } from './bonus' /** * 表格时间格式化 diff --git a/src/utils/request.js b/src/utils/request.js index 7150ecb..931d3a3 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -3,150 +3,250 @@ import { Notification, MessageBox, Message, Loading } from 'element-ui' import store from '@/store' import { getToken } from '@/utils/auth' import errorCode from '@/utils/errorCode' -import { tansParams, blobValidate } from "@/utils/ruoyi" +import { tansParams, blobValidate } from '@/utils/bonus' import cache from '@/plugins/cache' import { saveAs } from 'file-saver' +import { decryptWithSM4, encryptWithSM4, hashWithSM3AndSalt } from '@/utils/sm' + +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' -// 创建axios实例 + const service = axios.create({ - // axios中请求配置有baseURL选项,表示请求URL公共部分 - baseURL: process.env.VUE_APP_BASE_API, - // 超时 - timeout: 10000 + baseURL: process.env.VUE_APP_BASE_API, + timeout: 30000, }) -// request拦截器 -service.interceptors.request.use(config => { - // 是否需要设置 token - const isToken = (config.headers || {}).isToken === false - // 是否需要防止数据重复提交 - const isRepeatSubmit = (config.headers || {}).repeatSubmit === false - if (getToken() && !isToken) { - config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 - } - // get请求映射params参数 - if (config.method === 'get' && config.params) { - let url = config.url + '?' + tansParams(config.params) - url = url.slice(0, -1) - config.params = {} - config.url = url - } - if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { - const requestObj = { - url: config.url, - data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, - time: new Date().getTime() - } - const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小 - const limitSize = 5 * 1024 * 1024 // 限制存放数据5M - if (requestSize >= limitSize) { - console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') - return config - } - const sessionObj = cache.session.getJSON('sessionObj') - if (sessionObj === undefined || sessionObj === null || sessionObj === '') { - cache.session.setJSON('sessionObj', requestObj) - } else { - const s_url = sessionObj.url // 请求地址 - const s_data = sessionObj.data // 请求数据 - const s_time = sessionObj.time // 请求时间 - const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交 - if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { - const message = '数据正在处理,请勿重复提交' - console.warn(`[${s_url}]: ` + message) - return Promise.reject(new Error(message)) - } else { - cache.session.setJSON('sessionObj', requestObj) - } - } - } - return config -}, error => { - console.log(error) - Promise.reject(error) -}) +// request 拦截器 +service.interceptors.request.use( + (config) => { + const headers = config.headers || {} + const { + isToken = true, + encryptRequest = true, + checkIntegrity = true, + encryptResponse = true, + repeatSubmit = false, + } = headers -// 响应拦截器 -service.interceptors.response.use(res => { - // 未设置状态码则默认成功状态 - 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) { - isRelogin.show = true - MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { - isRelogin.show = false - store.dispatch('LogOut').then(() => { - location.href = '/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 => { - console.log('err' + error) - let { message } = error - if (message == "Network Error") { - message = "后端接口连接异常" - } else if (message.includes("timeout")) { - message = "系统接口请求超时" - } else if (message.includes("Request failed with status code")) { - message = "系统接口" + message.substr(message.length - 3) + "异常" - } - Message({ message: message, type: 'error', duration: 5 * 1000 }) - return Promise.reject(error) - } + // 设置请求头 + 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() + } + + // 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 { + // 不加密的情况,保持原有逻辑 + 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 data = typeof config.data === 'object' ? JSON.stringify(config.data) : config.data + let contentType = config.headers['Content-Type'] + + if (contentType && contentType.includes('application/json') && typeof data !== 'undefined') { + // 加密数据 + if (systemConfig.requestConfig.encryptRequest && encryptRequest) { + config.data = encryptWithSM4(data + '|' + hashWithSM3AndSalt(data)) + } + } + + // 检查请求数据大小 + const requestSize = JSON.stringify({ + url: config.url, + data: 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) { + 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 + if (message == 'Network Error') { + message = '后端接口连接异常' + } else if (message.includes('timeout')) { + message = '系统接口请求超时' + } else if (message.includes('Request failed with status code')) { + 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' }, - 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() - }) + 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 +export default service \ No newline at end of file diff --git a/src/utils/sm.js b/src/utils/sm.js new file mode 100644 index 0000000..8ed7008 --- /dev/null +++ b/src/utils/sm.js @@ -0,0 +1,50 @@ +// src/utils/encryption.js +import { sm2, sm3, sm4 } from 'sm-crypto' +// 配置项,例如盐值、SM2 公私钥、SM4 密钥 +import { SM_CONFIG } from './configure' +import SM4 from 'sm-crypto/src/sm4' +import { hexToArray } from 'sm-crypto/src/sm2/utils' + +// SM3 哈希 +export function hashSM3(text) { + // 对数据进行哈希计算 + return sm3(text) +} + +// 使用 SM3 进行哈希并加入盐值 +export function hashWithSM3AndSalt(text) { + // 将文本和盐值拼接在一起 + const textWithSalt = SM_CONFIG.SALT + text + // 使用 SM3 进行哈希 + return hashSM3(textWithSalt) +} + +// SM2 加密 +export function encryptWithSM2(text) { + // SM2 公钥加密 + return sm2.doEncrypt(text, SM_CONFIG.SM2_PUBLIC_KEY) +} + +// SM2 解密 +export function decryptWithSM2(encryptedText) { + // SM2 私钥解密 + return sm2.doDecrypt(encryptedText, SM_CONFIG.SM2_PRIVATE_KEY) +} +/** + * 加密函数 + * @param {string} plainText + * @returns {string} 加密后的密文(Hex 编码格式) + */ +export function encryptWithSM4(plainText) { + return sm4.encrypt(plainText, SM_CONFIG.SM4_KEY,{ mode: 'cbc', padding: 'pkcs#5',iv:SM_CONFIG.SM4_SALT}); +} + +/** + * 解密函数 + * @param {string} cipherText + * @returns {string} 解密后的明文 + */ +export function decryptWithSM4(cipherText){ + return SM4.decrypt(cipherText, SM_CONFIG.SM4_KEY,{ mode: 'cbc', padding: 'pkcs#5' ,iv:SM_CONFIG.SM4_SALT}); +} + diff --git a/src/views/index.vue b/src/views/index.vue index f33d8b7..6a8e712 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -1,1055 +1,6 @@ diff --git a/vue.config.js b/vue.config.js index 40e140d..5ea6f13 100644 --- a/vue.config.js +++ b/vue.config.js @@ -7,7 +7,7 @@ function resolve(dir) { const CompressionPlugin = require('compression-webpack-plugin') -const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题 +const name = process.env.VUE_APP_TITLE || '工程档案资料移交管理系统' // 网页标题 const baseUrl = 'http://localhost:8080' // 后端接口