diff --git a/package.json b/package.json index 3217898..039083c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ruoyi-vue-plus", - "version": "5.0.0", + "version": "5.1.0", "description": "RuoYi-Vue-Plus多租户管理系统", "author": "LionLi", "license": "MIT", @@ -29,6 +29,7 @@ "fuse.js": "6.6.2", "js-cookie": "3.0.1", "jsencrypt": "3.3.1", + "crypto-js": "^4.1.1", "nprogress": "0.2.0", "path-browserify": "1.0.1", "path-to-regexp": "6.2.0", @@ -44,6 +45,7 @@ "devDependencies": { "@iconify/json": "^2.2.40", "@intlify/unplugin-vue-i18n": "0.8.2", + "@types/crypto-js": "^4.1.1", "@types/file-saver": "2.0.5", "@types/js-cookie": "3.0.3", "@types/node": "18.14.2", diff --git a/src/api/login.ts b/src/api/login.ts index 9d6d1b7..2f75ecb 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -3,22 +3,24 @@ import { AxiosPromise } from 'axios'; import { LoginData, LoginResult, VerifyCodeResult, TenantInfo } from './types'; import { UserInfo } from '@/api/system/user/types'; +// pc端固定客户端授权id +const clientId = 'e5cd7e4891bf95d1d19206ce24a7b32e'; + /** * @param data {LoginData} * @returns */ export function login(data: LoginData): AxiosPromise { const params = { - tenantId: data.tenantId, - username: data.username.trim(), - password: data.password, - code: data.code, - uuid: data.uuid + ...data, + clientId: data.clientId || clientId, + grantType: data.grantType || 'password' }; return request({ url: '/auth/login', headers: { - isToken: false + isToken: false, + isEncrypt: true }, method: 'post', data: params @@ -60,19 +62,20 @@ export function getCodeImg(): AxiosPromise { timeout: 20000 }); } + /** * 第三方登录 - * @param source 第三方登录类型 - * */ -export function socialLogin(source: string, code: any, state: any): AxiosPromise { - const data = { - code, - state + */ +export function callback(data: LoginData): AxiosPromise { + const LoginData = { + ...data, + clientId: clientId, + grantType: 'social' }; return request({ - url: '/auth/social-login/' + source, - method: 'get', - params: data + url: '/auth/social/callback', + method: 'post', + data: LoginData }); } diff --git a/src/api/system/client/index.ts b/src/api/system/client/index.ts new file mode 100644 index 0000000..06544da --- /dev/null +++ b/src/api/system/client/index.ts @@ -0,0 +1,80 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; +import { ClientVO, ClientForm, ClientQuery } from '@/api/system/client/types'; + +/** + * 查询客户端管理列表 + * @param query + * @returns {*} + */ + +export const listClient = (query?: ClientQuery): AxiosPromise => { + return request({ + url: '/system/client/list', + method: 'get', + params: query + }); +}; + +/** + * 查询客户端管理详细 + * @param id + */ +export const getClient = (id: string | number): AxiosPromise => { + return request({ + url: '/system/client/' + id, + method: 'get' + }); +}; + +/** + * 新增客户端管理 + * @param data + */ +export const addClient = (data: ClientForm) => { + return request({ + url: '/system/client', + method: 'post', + data: data + }); +}; + +/** + * 修改客户端管理 + * @param data + */ +export const updateClient = (data: ClientForm) => { + return request({ + url: '/system/client', + method: 'put', + data: data + }); +}; + +/** + * 删除客户端管理 + * @param id + */ +export const delClient = (id: string | number | Array) => { + return request({ + url: '/system/client/' + id, + method: 'delete' + }); +}; + +/** + * 状态修改 + * @param id ID + * @param status 状态 + */ +export function changeStatus(id: number | string, status: string) { + const data = { + id, + status + }; + return request({ + url: '/system/client/changeStatus', + method: 'put', + data: data + }); +} diff --git a/src/api/system/client/types.ts b/src/api/system/client/types.ts new file mode 100644 index 0000000..e67f95f --- /dev/null +++ b/src/api/system/client/types.ts @@ -0,0 +1,138 @@ +export interface ClientVO { + /** + * id + */ + id: string | number; + + /** + * 客户端id + */ + clientId: string | number; + + /** + * 客户端key + */ + clientKey: string; + + /** + * 客户端秘钥 + */ + clientSecret: string; + + /** + * 授权类型 + */ + grantTypeList: string[]; + + /** + * 设备类型 + */ + deviceType: string; + + /** + * token活跃超时时间 + */ + activeTimeout: number; + + /** + * token固定超时 + */ + timeout: number; + + /** + * 状态(0正常 1停用) + */ + status: string; + +} + +export interface ClientForm extends BaseEntity { + /** + * id + */ + id?: string | number; + + /** + * 客户端id + */ + clientId?: string | number; + + /** + * 客户端key + */ + clientKey?: string; + + /** + * 客户端秘钥 + */ + clientSecret?: string; + + /** + * 授权类型 + */ + grantTypeList?: string[]; + + /** + * 设备类型 + */ + deviceType?: string; + + /** + * token活跃超时时间 + */ + activeTimeout?: number; + + /** + * token固定超时 + */ + timeout?: number; + + /** + * 状态(0正常 1停用) + */ + status?: string; + +} + +export interface ClientQuery extends PageQuery { + /** + * 客户端id + */ + clientId?: string | number; + + /** + * 客户端key + */ + clientKey?: string; + + /** + * 客户端秘钥 + */ + clientSecret?: string; + + /** + * 授权类型 + */ + grantType?: string; + + /** + * 设备类型 + */ + deviceType?: string; + + /** + * token活跃超时时间 + */ + activeTimeout?: number; + + /** + * token固定超时 + */ + timeout?: number; + + /** + * 状态(0正常 1停用) + */ + status?: string; + +} diff --git a/src/api/types.ts b/src/api/types.ts index 68fb427..e02e645 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -15,19 +15,24 @@ export type RegisterForm = { * 登录请求 */ export interface LoginData { - tenantId: string; - username: string; - password: string; + tenantId?: string; + username?: string; + password?: string; rememberMe?: boolean; + socialCode?: string, + socialState?: string, + source?: string, code?: string; uuid?: string; + clientId: string; + grantType: string; } /** * 登录响应 */ export interface LoginResult { - token: string; + access_token: string; } /** diff --git a/src/components/RuoYiDoc/index.vue b/src/components/RuoYiDoc/index.vue index 6dad85c..6c7ac0d 100644 --- a/src/components/RuoYiDoc/index.vue +++ b/src/components/RuoYiDoc/index.vue @@ -5,7 +5,7 @@ diff --git a/src/layout/components/SocialLogin/index.vue b/src/layout/components/SocialLogin/index.vue deleted file mode 100644 index fad8286..0000000 --- a/src/layout/components/SocialLogin/index.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue index 949e096..b50c628 100644 --- a/src/layout/components/TagsView/ScrollPane.vue +++ b/src/layout/components/TagsView/ScrollPane.vue @@ -95,7 +95,7 @@ defineExpose({ bottom: 0px; } :deep(.el-scrollbar__wrap) { - height: 39px; + height: 49px; } } diff --git a/src/permission.ts b/src/permission.ts index c2743eb..4543d08 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -10,7 +10,7 @@ import useSettingsStore from '@/store/modules/settings'; import usePermissionStore from '@/store/modules/permission'; NProgress.configure({ showSpinner: false }); -const whiteList = ['/login', '/register', '/social-login']; +const whiteList = ['/login', '/register', '/social-callback']; router.beforeEach(async (to, from, next) => { NProgress.start(); diff --git a/src/plugins/download.ts b/src/plugins/download.ts index e1c4414..c6c8521 100644 --- a/src/plugins/download.ts +++ b/src/plugins/download.ts @@ -34,21 +34,29 @@ export default { }, async zip(url: string, name: string) { url = baseURL + url; - const res = await axios({ - method: 'get', - url: url, - responseType: 'blob', - headers: { - Authorization: 'Bearer ' + getToken(), - datasource: localStorage.getItem('dataName') + downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); + try { + const res = await axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { + Authorization: 'Bearer ' + getToken(), + datasource: localStorage.getItem('dataName') + } + }); + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data], { type: 'application/zip' }); + FileSaver.saveAs(blob, name); + } else { + this.printErrMsg(res.data); } - }); - const isBlob = blobValidate(res.data); - if (isBlob) { - const blob = new Blob([res.data], { type: 'application/zip' }); - FileSaver.saveAs(blob, name); - } else { - this.printErrMsg(res.data); + downloadLoadingInstance.close(); + } catch (r) { + console.error(r) + ElMessage.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close(); } }, async printErrMsg(data: any) { diff --git a/src/router/index.ts b/src/router/index.ts index 76a81bc..d6aefe9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -38,9 +38,9 @@ export const constantRoutes: RouteOption[] = [ ] }, { - path: '/social-login', + path: '/social-callback', hidden: true, - component: () => import('@/layout/components/SocialLogin/index.vue') + component: () => import('@/layout/components/SocialCallback/index.vue') }, { path: '/login', @@ -181,4 +181,5 @@ const router = createRouter({ } }); + export default router; diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 6f30437..2593d1a 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -23,8 +23,8 @@ export const useUserStore = defineStore('user', () => { const [err, res] = await to(loginApi(userInfo)); if (res) { const data = res.data; - setToken(data.token); - token.value = data.token; + setToken(data.access_token); + token.value = data.access_token; return Promise.resolve(); } return Promise.reject(err); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 4020003..db50ac9 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -4,6 +4,6 @@ const tokenStorage = useStorage(TokenKey, null); export const getToken = () => tokenStorage.value; -export const setToken = (token: string) => (tokenStorage.value = token); +export const setToken = (access_token: string) => (tokenStorage.value = access_token); export const removeToken = () => (tokenStorage.value = null); diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 0000000..133893e --- /dev/null +++ b/src/utils/crypto.ts @@ -0,0 +1,45 @@ +import CryptoJS from 'crypto-js'; + +/** + * 随机生成32位的字符串 + * @returns {string} + */ +const generateRandomString = () => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + const charactersLength = characters.length; + for (let i = 0; i < 32; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}; + +/** + * 随机生成aes 密钥 + * @returns {string} + */ +export const generateAesKey = () => { + return CryptoJS.enc.Utf8.parse(generateRandomString()); +}; + +/** + * 加密base64 + * @returns {string} + */ +export const encryptBase64 = (str: CryptoJS.lib.WordArray) => { + return CryptoJS.enc.Base64.stringify(str); +}; + +/** + * 使用密钥对数据进行加密 + * @param message + * @param aesKey + * @returns {string} + */ +export const encryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray) => { + const encrypted = CryptoJS.AES.encrypt(message, aesKey, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + return encrypted.toString(); +}; diff --git a/src/utils/jsencrypt.ts b/src/utils/jsencrypt.ts index 18493ad..2d93757 100644 --- a/src/utils/jsencrypt.ts +++ b/src/utils/jsencrypt.ts @@ -2,7 +2,8 @@ import JSEncrypt from 'jsencrypt'; // 密钥对生成 http://web.chacuo.net/netrsakeypair const publicKey = - 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='; + 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='; const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + diff --git a/src/utils/request.ts b/src/utils/request.ts index d5fac4e..74f0d19 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -8,6 +8,8 @@ import { errorCode } from '@/utils/errorCode'; import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'; import FileSaver from 'file-saver'; import { getLanguage } from '@/lang'; +import { encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto'; +import { encrypt } from '@/utils/jsencrypt'; let downloadLoadingInstance: LoadingInstance; // 是否显示重新登录 @@ -29,6 +31,8 @@ service.interceptors.request.use( const isToken = (config.headers || {}).isToken === false; // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false; + // 是否需要加密 + const isEncrypt = (config.headers || {}).isEncrypt === 'true'; if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 } @@ -63,6 +67,13 @@ service.interceptors.request.use( } } } + // 当开启参数加密 + if (isEncrypt && (config.method === 'post' || config.method === 'put')) { + // 生成一个 AES 密钥 + const aesKey = generateAesKey(); + config.headers['encrypt-key'] = encrypt(encryptBase64(aesKey)); + config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey); + } // FormData数据去请求头Content-Type if (config.data instanceof FormData) { delete config.headers['Content-Type']; diff --git a/src/views/index.vue b/src/views/index.vue index 4b3bebd..438c1af 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -33,14 +33,14 @@ * 部署方式 Docker 容器编排 一键部署业务集群
* 国际化 SpringMessage Spring标准国际化方案

-

当前版本: v5.0.0

+

当前版本: v5.1.0

¥免费开源

访问码云 访问GitHub - 更新日志

@@ -78,14 +78,14 @@ * 分布式监控 Prometheus、Grafana 全方位性能监控
* 其余与 Vue 版本一致

-

当前版本: v2.0.0

+

当前版本: v2.1.0

¥免费开源

访问码云 访问GitHub - 更新日志

diff --git a/src/views/login.vue b/src/views/login.vue index 709b0ce..4c8715a 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -72,7 +72,7 @@ const userStore = useUserStore(); const router = useRouter(); const loginForm = ref({ - tenantId: "000000", + tenantId: '000000', username: 'admin', password: 'admin123', rememberMe: false, @@ -176,6 +176,12 @@ const initTenantList = async () => { } } } + +//检测租户选择框的变化 +watch(() => loginForm.value.tenantId, (val: string) => { + Cookies.set("tenantId", loginForm.value.tenantId, { expires: 30 }) +}); + /** * 第三方登录 * @param type @@ -183,8 +189,9 @@ const initTenantList = async () => { const doSocialLogin = (type: string) => { authBinding(type).then((res: any) => { if (res.code === HttpStatus.SUCCESS) { - window.location.href = res.msg; - } else { + // 获取授权地址跳转 + window.location.href = res.data; + } else { ElMessage.error(res.msg); } }); diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue index 2914d59..8719c94 100644 --- a/src/views/monitor/cache/index.vue +++ b/src/views/monitor/cache/index.vue @@ -133,56 +133,59 @@ const usedmemory = ref(); const { proxy } = getCurrentInstance() as ComponentInternalInstance; const getList = async () => { - proxy?.$modal.loading("正在加载缓存监控数据,请稍候!"); - const res = await getCache(); - proxy?.$modal.closeLoading(); - cache.value = res.data; - const commandstatsIntance = echarts.init(commandstats.value, "macarons"); - commandstatsIntance.setOption({ - tooltip: { - trigger: "item", - formatter: "{a}
{b} : {c} ({d}%)" + proxy?.$modal.loading("正在加载缓存监控数据,请稍候!"); + const res = await getCache(); + proxy?.$modal.closeLoading(); + cache.value = res.data; + const commandstatsIntance = echarts.init(commandstats.value, "macarons"); + commandstatsIntance.setOption({ + tooltip: { + trigger: "item", + formatter: "{a}
{b} : {c} ({d}%)" + }, + series: [ + { + name: "命令", + type: "pie", + roseType: "radius", + radius: [15, 95], + center: ["50%", "38%"], + data: res.data.commandStats, + animationEasing: "cubicInOut", + animationDuration: 1000 + } + ] + }); + const usedmemoryInstance = echarts.init(usedmemory.value, "macarons"); + usedmemoryInstance.setOption({ + tooltip: { + formatter: "{b}
{a} : " + cache.value.info.used_memory_human + }, + series: [ + { + name: "峰值", + type: "gauge", + min: 0, + max: 1000, + detail: { + formatter: cache.value.info.used_memory_human }, - series: [ - { - name: "命令", - type: "pie", - roseType: "radius", - radius: [15, 95], - center: ["50%", "38%"], - data: res.data.commandStats, - animationEasing: "cubicInOut", - animationDuration: 1000 - } + data: [ + { + value: parseFloat(cache.value.info.used_memory_human), + name: "内存消耗" + } ] - }); - - const usedmemoryInstance = echarts.init(usedmemory.value, "macarons"); - usedmemoryInstance.setOption({ - tooltip: { - formatter: "{b}
{a} : " + cache.value.info.used_memory_human - }, - series: [ - { - name: "峰值", - type: "gauge", - min: 0, - max: 1000, - detail: { - formatter: cache.value.info.used_memory_human - }, - data: [ - { - value: parseFloat(cache.value.info.used_memory_human), - name: "内存消耗" - } - ] - } - ] - }) + } + ] + }) + window.addEventListener("resize",()=>{ + commandstatsIntance.resize() + usedmemoryInstance.resize() + }); } onMounted(() => { - getList(); + getList(); }) diff --git a/src/views/system/client/index.vue b/src/views/system/client/index.vue new file mode 100644 index 0000000..2130d03 --- /dev/null +++ b/src/views/system/client/index.vue @@ -0,0 +1,344 @@ + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 00979c9..3b496ea 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -181,7 +181,7 @@ - +