瀏覽代碼

feat: the cache can be configured to be encrypted

vben 4 年之前
父節點
當前提交
234c1d1fae

+ 5 - 0
CHANGELOG.zh_CN.md

@@ -1,5 +1,10 @@
 ## Wip
 
+### ✨ Features
+
+- 缓存可以配置是否加密
+- 多语言支持
+
 ### 🎫 Chores
 
 - 移除 messageSetting 配置

+ 4 - 4
package.json

@@ -37,16 +37,16 @@
     "vditor": "^3.6.3",
     "vue": "^3.0.2",
     "vue-i18n": "^9.0.0-beta.8",
-    "vue-router": "^4.0.0-rc.3",
+    "vue-router": "^4.0.0-rc.5",
     "vuex": "^4.0.0-rc.1",
     "vuex-module-decorators": "^1.0.1",
-    "xlsx": "^0.16.8",
+    "xlsx": "^0.16.9",
     "zxcvbn": "^4.4.2"
   },
   "devDependencies": {
     "@commitlint/cli": "^11.0.0",
     "@commitlint/config-conventional": "^11.0.0",
-    "@iconify/json": "^1.1.261",
+    "@iconify/json": "^1.1.262",
     "@ls-lint/ls-lint": "^1.9.2",
     "@purge-icons/generated": "^0.4.1",
     "@types/echarts": "^4.9.1",
@@ -72,7 +72,7 @@
     "cross-env": "^7.0.2",
     "dot-prop": "^6.0.1",
     "dotenv": "^8.2.0",
-    "eslint": "^7.13.0",
+    "eslint": "^7.14.0",
     "eslint-config-prettier": "^6.15.0",
     "eslint-plugin-prettier": "^3.1.4",
     "eslint-plugin-vue": "^7.1.0",

+ 5 - 1
src/components/Application/index.ts

@@ -1,3 +1,7 @@
 import AppLocalPicker from './src/AppLocalPicker.vue';
+import AppFooterToolbar from './src/AppFooterToolbar.vue';
+import { withInstall } from '../util';
 
-export { AppLocalPicker };
+export { AppLocalPicker, AppFooterToolbar };
+
+export default withInstall(AppLocalPicker, AppFooterToolbar);

+ 4 - 1
src/components/Footer/src/index.vue → src/components/Application/src/AppFooterToolbar.vue

@@ -10,11 +10,14 @@
 </template>
 <script lang="ts">
   import { defineComponent, computed, unref } from 'vue';
+
   import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
+
   import { appStore } from '/@/store/modules/app';
   import { menuStore } from '/@/store/modules/menu';
+
   export default defineComponent({
-    name: 'AppFooter',
+    name: 'AppFooterToolbar',
     setup() {
       const getMiniWidth = computed(() => {
         const {

+ 3 - 4
src/components/Authority/index.ts

@@ -1,8 +1,7 @@
-import type { App } from 'vue';
 import Authority from './src/index.vue';
 
-export default (app: App): void => {
-  app.component(Authority.name, Authority);
-};
+import { withInstall } from '../util';
+
+export default withInstall(Authority);
 
 export { Authority };

+ 0 - 1
src/components/Footer/index.ts

@@ -1 +0,0 @@
-export { default as AppFooter } from './src/index.vue';

+ 1 - 2
src/components/registerGlobComp.ts

@@ -1,6 +1,5 @@
 import Icon from './Icon/index';
 import Button from './Button/index.vue';
-import { AppFooter } from './Footer';
 import {
   // Need
   Button as AntButton,
@@ -35,7 +34,7 @@ import {
 } from 'ant-design-vue';
 import { getApp } from '/@/setup/App';
 
-const compList = [Icon, Button, AntButton.Group, AppFooter];
+const compList = [Icon, Button, AntButton.Group];
 
 // Fix hmr multiple registered components
 let registered = false;

+ 9 - 0
src/components/util.ts

@@ -1,4 +1,13 @@
 import type { VNodeChild } from 'vue';
+import type { App, Component } from 'vue';
+
+export function withInstall(...components: Component[]) {
+  return (app: App) => {
+    components.forEach((comp) => {
+      comp.name && app.component(comp.name, comp);
+    });
+  };
+}
 
 export function convertToUnit(
   str: string | number | null | undefined,

+ 0 - 10
src/settings/cipherSetting.ts

@@ -1,10 +0,0 @@
-// System default cache time, in seconds
-export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
-
-/**
- * @description:
- */
-export const storageCipher = {
-  key: '_12345678901234@',
-  iv: '@12345678901234_',
-};

+ 13 - 0
src/settings/encryptionSetting.ts

@@ -0,0 +1,13 @@
+import { isDevMode } from '/@/utils/env';
+
+// System default cache time, in seconds
+export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
+
+// aes encryption key
+export const cacheCipher = {
+  key: '_12345678901234@',
+  iv: '@12345678901234_',
+};
+
+// Whether the system cache is encrypted using aes
+export const enableStorageEncryption = !isDevMode();

+ 78 - 0
src/utils/cache/cookie.ts

@@ -0,0 +1,78 @@
+import { DEFAULT_CACHE_TIME } from '../../settings/encryptionSetting';
+import { getStorageShortName } from '/@/utils/helper/envHelper';
+import { cacheCipher } from '/@/settings/encryptionSetting';
+import Encryption from '/@/utils/encryption/aesEncryption';
+
+export default class WebCookie {
+  private encryption: Encryption;
+  private hasEncrypt: boolean;
+
+  constructor(hasEncrypt = true, key = cacheCipher.key, iv = cacheCipher.iv) {
+    const encryption = new Encryption({ key, iv });
+    this.encryption = encryption;
+    this.hasEncrypt = hasEncrypt;
+  }
+
+  private getKey(key: string) {
+    return `${getStorageShortName()}${key}`.toUpperCase();
+  }
+
+  /**
+   * Add cookie
+   * @param name cookie key
+   * @param value cookie value
+   * @param expire
+   * If the expiration time is not set, the default management browser will automatically delete
+   * e.g:
+   *  cookieData.set('name','value',)
+   */
+  setCookie(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
+    value = this.hasEncrypt ? this.encryption.encryptByAES(JSON.stringify(value)) : value;
+    document.cookie = this.getKey(key) + '=' + value + '; Max-Age=' + expire;
+  }
+
+  /**
+   * Get the cook value according to the key
+   * @param key cookie key
+   */
+  getCookie(key: string) {
+    const arr = document.cookie.split('; ');
+    for (let i = 0; i < arr.length; i++) {
+      const arr2 = arr[i].split('=');
+      if (arr2[0] === this.getKey(key)) {
+        let message: any = null;
+        const str = arr2[1];
+        if (this.hasEncrypt && str) {
+          message = this.encryption.decryptByAES(str);
+          try {
+            return JSON.parse(message);
+          } catch (e) {
+            return str;
+          }
+        }
+        return str;
+      }
+    }
+    return '';
+  }
+
+  /**
+   * Delete cookie based on cookie key
+   * @param key cookie key
+   */
+  removeCookie(key: string) {
+    this.setCookie(key, 1, -1);
+  }
+
+  /**
+   * clear cookie
+   */
+  clearCookie(): void {
+    const keys = document.cookie.match(/[^ =;]+(?==)/g);
+    if (keys) {
+      for (let i = keys.length; i--; ) {
+        document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString();
+      }
+    }
+  }
+}

+ 6 - 3
src/utils/storage/index.ts → src/utils/cache/index.ts

@@ -1,17 +1,20 @@
 import { getStorageShortName } from '/@/utils/helper/envHelper';
-import { createStorage as create } from './Storage';
-
-// debug模式下不加密
+import { createStorage as create } from './storageCache';
+import { enableStorageEncryption } from '/@/settings/encryptionSetting';
 
 const createOptions = (storage = sessionStorage) => {
   return {
+    // No encryption in debug mode
+    hasEncrypt: enableStorageEncryption,
     storage,
     prefixKey: getStorageShortName(),
   };
 };
+
 export const WebStorage = create(createOptions());
 
 export const createStorage = (storage: Storage = sessionStorage) => {
   return create(createOptions(storage))!;
 };
+
 export default WebStorage;

+ 108 - 0
src/utils/cache/storageCache.ts

@@ -0,0 +1,108 @@
+import { DEFAULT_CACHE_TIME } from '/@/settings/encryptionSetting';
+import { cacheCipher } from '/@/settings/encryptionSetting';
+import Encryption, { EncryptionParams } from '/@/utils/encryption/aesEncryption';
+
+export interface CreateStorageParams extends EncryptionParams {
+  storage: Storage;
+
+  hasEncrypt: boolean;
+}
+export const createStorage = ({
+  prefixKey = '',
+  storage = sessionStorage,
+  key = cacheCipher.key,
+  iv = cacheCipher.iv,
+  hasEncrypt = true,
+} = {}) => {
+  if (hasEncrypt && [key.length, iv.length].some((item) => item !== 16)) {
+    throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!');
+  }
+
+  const encryption = new Encryption({ key, iv });
+
+  /**
+   *Cache class
+   *Construction parameters can be passed into sessionStorage, localStorage,
+   * @class Cache
+   * @example
+   */
+  const WebStorage = class WebStorage {
+    private storage: Storage;
+    private prefixKey?: string;
+    private encryption: Encryption;
+    private hasEncrypt: boolean;
+    /**
+     *
+     * @param {*} storage
+     */
+    constructor() {
+      this.storage = storage;
+      this.prefixKey = prefixKey;
+      this.encryption = encryption;
+      this.hasEncrypt = hasEncrypt;
+    }
+
+    private getKey(key: string) {
+      return `${this.prefixKey}${key}`.toUpperCase();
+    }
+
+    /**
+     *
+     *  Set cache
+     * @param {string} key
+     * @param {*} value
+     * @expire Expiration time in seconds
+     * @memberof Cache
+     */
+    set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
+      const stringData = JSON.stringify({
+        value,
+        expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
+      });
+      const stringifyValue = this.hasEncrypt
+        ? this.encryption.encryptByAES(stringData)
+        : stringData;
+      this.storage.setItem(this.getKey(key), stringifyValue);
+    }
+
+    /**
+     *Read cache
+     * @param {string} key
+     * @memberof Cache
+     */
+    get(key: string, def: any = null): any {
+      const item = this.storage.getItem(this.getKey(key));
+      if (item) {
+        try {
+          const decItem = this.hasEncrypt ? this.encryption.decryptByAES(item) : item;
+          const data = JSON.parse(decItem);
+          const { value, expire } = data;
+          if (expire === null || expire >= new Date().getTime()) {
+            return value;
+          }
+          this.remove(this.getKey(key));
+        } catch (e) {
+          return def;
+        }
+      }
+      return def;
+    }
+
+    /**
+     * Delete cache based on key
+     * @param {string} key
+     * @memberof Cache
+     */
+    remove(key: string) {
+      this.storage.removeItem(this.getKey(key));
+    }
+
+    /**
+     * Delete all caches of this instance
+     */
+    clear(): void {
+      this.storage.clear();
+    }
+  };
+  return new WebStorage();
+};

+ 35 - 0
src/utils/encryption/aesEncryption.ts

@@ -0,0 +1,35 @@
+import CryptoES from 'crypto-es';
+export interface EncryptionParams {
+  key: string;
+  iv: string;
+}
+export class Encryption {
+  private key;
+
+  private iv;
+
+  constructor(opt: EncryptionParams) {
+    const { key, iv } = opt;
+    this.key = CryptoES.enc.Utf8.parse(key);
+    this.iv = CryptoES.enc.Utf8.parse(iv);
+  }
+
+  get getOpt(): CryptoES.lib.CipherCfg {
+    return {
+      mode: CryptoES.mode.CBC as any,
+      padding: CryptoES.pad.Pkcs7,
+      iv: this.iv,
+    };
+  }
+
+  encryptByAES(str: string) {
+    const encrypted = CryptoES.AES.encrypt(str, this.key, this.getOpt);
+    return encrypted.toString();
+  }
+
+  decryptByAES(str: string) {
+    const decrypted = CryptoES.AES.decrypt(str, this.key, this.getOpt);
+    return decrypted.toString(CryptoES.enc.Utf8);
+  }
+}
+export default Encryption;

+ 0 - 0
src/utils/file/base64.ts → src/utils/file/base64Conver.ts


+ 1 - 1
src/utils/file/download.ts

@@ -1,4 +1,4 @@
-import { dataURLtoBlob, urlToBase64 } from './base64';
+import { dataURLtoBlob, urlToBase64 } from './base64Conver';
 
 /**
  * Download online pictures

+ 1 - 1
src/utils/helper/persistent.ts

@@ -1,4 +1,4 @@
-import { createStorage } from '/@/utils/storage';
+import { createStorage } from '/@/utils/cache';
 import { isIeFn } from '/@/utils/browser';
 
 import { BASE_LOCAL_CACHE_KEY, BASE_SESSION_CACHE_KEY } from '/@/enums/cacheEnum';

+ 0 - 138
src/utils/storage/Storage.ts

@@ -1,138 +0,0 @@
-import { DEFAULT_CACHE_TIME } from '/@/settings/cipherSetting';
-
-// import { EncryptionParams } from '/@/utils/cipher/aesEncryption';
-export interface CreateStorageParams {
-  storage: Storage;
-  hasEncrypt: boolean;
-}
-export const createStorage = ({ prefixKey = '', storage = sessionStorage } = {}) => {
-  /**
-   *缓存类
-   *构造参数可以传入 sessionStorage,localStorage,
-   * @class Cache
-   * @example
-   */
-  const WebStorage = class WebStorage {
-    private storage: Storage;
-    private prefixKey?: string;
-
-    /**
-     *
-     * @param {*} storage
-     */
-    constructor() {
-      this.storage = storage;
-      this.prefixKey = prefixKey;
-    }
-
-    private getKey(key: string) {
-      return `${this.prefixKey}${key}`.toUpperCase();
-    }
-
-    /**
-     *
-     *  设置缓存
-     * @param {string} key 缓存键
-     * @param {*} value 缓存值
-     * @expire 过期时间 单位秒
-     * @memberof Cache
-     */
-    set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
-      const stringData = JSON.stringify({
-        value,
-        expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
-      });
-      this.storage.setItem(this.getKey(key), stringData);
-    }
-
-    /**
-     *
-     *读取缓存
-     * @param {string} key 缓存键
-     * @returns 缓存值
-     * @memberof Cache
-     */
-    get(key: string, def: any = null): any {
-      const item = this.storage.getItem(this.getKey(key));
-      if (item) {
-        try {
-          const data = JSON.parse(item);
-          const { value, expire } = data;
-          if (expire === null || expire >= new Date().getTime()) {
-            return value;
-          }
-          this.remove(this.getKey(key));
-        } catch (e) {
-          return def;
-        }
-      }
-      return def;
-    }
-
-    /**
-     *
-     *删除缓存
-     * @param {string} key 缓存键
-     * @memberof Cache
-     */
-    remove(key: string) {
-      this.storage.removeItem(this.getKey(key));
-    }
-
-    /**
-     *
-     *删除该实例所有缓存
-     * @memberof Cache
-     */
-    clear(): void {
-      this.storage.clear();
-    }
-
-    /**
-     * 添加cookie
-     * @param name cookie名字
-     * @param value cookie内容
-     * @param expire
-     * 如果过期时间未设置,默认管理浏览器自动删除
-     * 例子:
-     *  cookieData.set('name','value',)
-     */
-    setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
-      document.cookie = this.getKey(name) + '=' + value + '; Max-Age=' + expire;
-    }
-
-    /**
-     * 根据名字获取cooki值
-     * @param name cookie名
-     * @returns {*} cookie值
-     */
-    getCookie(name: string) {
-      const arr = document.cookie.split('; ');
-      for (let i = 0; i < arr.length; i++) {
-        const arr2 = arr[i].split('=');
-        if (arr2[0] === this.getKey(name)) {
-          return arr2[1];
-        }
-      }
-      return '';
-    }
-
-    /**
-     * 根据cookie名字删除cookie
-     * @param name cookie名字
-     */
-    removeCookie(key: string) {
-      this.setCookie(key, 1, -1);
-    }
-
-    clearCookie(): void {
-      const keys = document.cookie.match(/[^ =;]+(?==)/g);
-      if (keys) {
-        for (let i = keys.length; i--; ) {
-          document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString();
-        }
-      }
-    }
-  };
-  return new WebStorage();
-};

+ 5 - 3
src/views/demo/page/form/high/index.vue

@@ -16,21 +16,23 @@
       </a-card>
     </div>
 
-    <app-footer>
+    <AppFooterToolbar>
       <template #right>
         <a-button type="primary" @click="submitAll">提交</a-button>
       </template>
-    </app-footer>
+    </AppFooterToolbar>
   </div>
 </template>
 <script lang="ts">
   import { BasicForm, useForm } from '/@/components/Form';
   import { defineComponent, ref } from 'vue';
   import PersonTable from './PersonTable.vue';
+  import { AppFooterToolbar } from '/@/components/Application';
+
   import { schemas, taskSchemas } from './data';
 
   export default defineComponent({
-    components: { BasicForm, PersonTable },
+    components: { BasicForm, PersonTable, AppFooterToolbar },
     setup() {
       const tableRef = ref<{ getDataSource: () => any } | null>(null);
 

+ 22 - 16
yarn.lock

@@ -1050,10 +1050,10 @@
   resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b"
   integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
 
-"@iconify/json@^1.1.261":
-  version "1.1.261"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.261.tgz#9a6986b6b36d77ca147c4be149db9a43280a8fb2"
-  integrity sha512-lnRk1OBqNxZ593oZyOXEMp/O+cr+lF54xaW6+F3krWdWhzxQgi0W1ffzvdiLySdbTEorQ2NvVU4e0+Af27rXPA==
+"@iconify/json@^1.1.262":
+  version "1.1.262"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.262.tgz#a67067bad418d59c729ec514e3aa629d0ab3710b"
+  integrity sha512-PfKUS/Ue9Rn2oO0ez/Yj4Cdodvv6vDHgPjIZNSElu2+149CYaPmWahHI87ZY+r8l3bijPIu6+blyAixdJtPPMg==
 
 "@koa/cors@^3.1.0":
   version "3.1.0"
@@ -3418,10 +3418,10 @@ eslint-visitor-keys@^2.0.0:
   resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
   integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
 
-eslint@^7.13.0:
-  version "7.13.0"
-  resolved "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz#7f180126c0dcdef327bfb54b211d7802decc08da"
-  integrity sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==
+eslint@^7.14.0:
+  version "7.14.0"
+  resolved "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz#2d2cac1d28174c510a97b377f122a5507958e344"
+  integrity sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     "@eslint/eslintrc" "^0.2.1"
@@ -3686,6 +3686,11 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
+fflate@^0.3.8:
+  version "0.3.10"
+  resolved "https://registry.npmjs.org/fflate/-/fflate-0.3.10.tgz#0e581839a53203d2eeac7e61ce3652d855e24dcd"
+  integrity sha512-s5j69APkUPPbzdI20Ix4pPtQP+1Qi58YcFRpE7aO/P1kEywUYjbl2RjZRVEMdnySO9pr4MB0BHPbxkiahrtD/Q==
+
 figures@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -8196,10 +8201,10 @@ vue-i18n@^9.0.0-beta.8:
   dependencies:
     source-map "0.6.1"
 
-vue-router@^4.0.0-rc.3:
-  version "4.0.0-rc.3"
-  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.3.tgz#70d18e90030bc6a25e81a30401d673223998ec6b"
-  integrity sha512-NnPqWIfanEhJC4wu8BEFBmnEDIrx9ST0/HtmBiE+oV2MQlhyRk1TmdttWwVqx6Sh7kONsrI10GQV9l3YEkcWXg==
+vue-router@^4.0.0-rc.5:
+  version "4.0.0-rc.5"
+  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.5.tgz#191d32e3d5276641ff21e881d34e33a71dc6e8f0"
+  integrity sha512-Q8Tt6VGwGMN5qASeIdjSydU3uRADK9AUkqnbnzmTz+zZKS0W6GZOAuP235lf3y5/MqEFSKRJGaTWPEY0t+Rjmg==
 
 vue-types@^3.0.0:
   version "3.0.1"
@@ -8480,10 +8485,10 @@ ws@^7.3.1:
   resolved "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7"
   integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==
 
-xlsx@^0.16.8:
-  version "0.16.8"
-  resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.16.8.tgz#5546de9b0ba15169b36770d4e43b24790d3ff1b8"
-  integrity sha512-qWub4YCn0xLEGHI7WWhk6IJ73MDu7sPSJQImxN6/LiI8wsHi0hUhICEDbyqBT+jgFgORZxrii0HvhNSwBNAPoQ==
+xlsx@^0.16.9:
+  version "0.16.9"
+  resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.16.9.tgz#dacd5bb46bda6dd3743940c9c3dc1e2171826256"
+  integrity sha512-gxi1I3EasYvgCX1vN9pGyq920Ron4NO8PNfhuoA3Hpq6Y8f0ECXiy4OLrK4QZBnj1jx3QD+8Fq5YZ/3mPZ5iXw==
   dependencies:
     adler-32 "~1.2.0"
     cfb "^1.1.4"
@@ -8491,6 +8496,7 @@ xlsx@^0.16.8:
     commander "~2.17.1"
     crc-32 "~1.2.0"
     exit-on-epipe "~1.0.1"
+    fflate "^0.3.8"
     ssf "~0.11.2"
     wmf "~1.0.1"
     word "~0.3.0"