feat: encrypt the privacy data when it is persisted (#6056)
* 对私密数据持久化时执行加密 * 将锁屏密码合并到accessStore中进行加密pull/84/head
							parent
							
								
									9ee6d06d50
								
							
						
					
					
						commit
						aa27a2f7a1
					
				|  | @ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Antd | |||
| 
 | ||||
| # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 | ||||
| VITE_APP_NAMESPACE=vben-web-antd | ||||
| 
 | ||||
| # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 | ||||
| VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key | ||||
|  |  | |||
|  | @ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Ele | |||
| 
 | ||||
| # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 | ||||
| VITE_APP_NAMESPACE=vben-web-ele | ||||
| 
 | ||||
| # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 | ||||
| VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key | ||||
|  |  | |||
|  | @ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Naive | |||
| 
 | ||||
| # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 | ||||
| VITE_APP_NAMESPACE=vben-web-naive | ||||
| 
 | ||||
| # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 | ||||
| VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ import { | |||
|   updatePreferences, | ||||
|   usePreferences, | ||||
| } from '@vben/preferences'; | ||||
| import { useLockStore } from '@vben/stores'; | ||||
| import { useAccessStore } from '@vben/stores'; | ||||
| import { cloneDeep, mapTree } from '@vben/utils'; | ||||
| 
 | ||||
| import { VbenAdminLayout } from '@vben-core/layout-ui'; | ||||
|  | @ -49,7 +49,7 @@ const { | |||
|   sidebarCollapsed, | ||||
|   theme, | ||||
| } = usePreferences(); | ||||
| const lockStore = useLockStore(); | ||||
| const accessStore = useAccessStore(); | ||||
| const { refresh } = useRefresh(); | ||||
| 
 | ||||
| const sidebarTheme = computed(() => { | ||||
|  | @ -356,7 +356,7 @@ const headerSlots = computed(() => { | |||
|       /> | ||||
| 
 | ||||
|       <Transition v-if="preferences.widget.lockScreen" name="slide-up"> | ||||
|         <slot v-if="lockStore.isLockScreen" name="lock-screen"></slot> | ||||
|         <slot v-if="accessStore.isLockScreen" name="lock-screen"></slot> | ||||
|       </Transition> | ||||
| 
 | ||||
|       <template v-if="preferencesButtonPosition.fixed"> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { computed, reactive, ref } from 'vue'; | |||
| 
 | ||||
| import { LockKeyhole } from '@vben/icons'; | ||||
| import { $t, useI18n } from '@vben/locales'; | ||||
| import { storeToRefs, useLockStore } from '@vben/stores'; | ||||
| import { storeToRefs, useAccessStore } from '@vben/stores'; | ||||
| 
 | ||||
| import { useScrollLock } from '@vben-core/composables'; | ||||
| import { useVbenForm, z } from '@vben-core/form-ui'; | ||||
|  | @ -26,7 +26,7 @@ withDefaults(defineProps<Props>(), { | |||
| defineEmits<{ toLogin: [] }>(); | ||||
| 
 | ||||
| const { locale } = useI18n(); | ||||
| const lockStore = useLockStore(); | ||||
| const accessStore = useAccessStore(); | ||||
| 
 | ||||
| const now = useNow(); | ||||
| const meridiem = useDateFormat(now, 'A'); | ||||
|  | @ -35,7 +35,7 @@ const minute = useDateFormat(now, 'mm'); | |||
| const date = useDateFormat(now, 'YYYY-MM-DD dddd', { locales: locale.value }); | ||||
| 
 | ||||
| const showUnlockForm = ref(false); | ||||
| const { lockScreenPassword } = storeToRefs(lockStore); | ||||
| const { lockScreenPassword } = storeToRefs(accessStore); | ||||
| 
 | ||||
| const [Form, { form, validate }] = useVbenForm( | ||||
|   reactive({ | ||||
|  | @ -66,7 +66,7 @@ async function handleSubmit() { | |||
|   const { valid } = await validate(); | ||||
|   if (valid) { | ||||
|     if (validPass.value) { | ||||
|       lockStore.unlockScreen(); | ||||
|       accessStore.unlockScreen(); | ||||
|     } else { | ||||
|       form.setFieldError('password', $t('authentication.passwordErrorTip')); | ||||
|     } | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import { useHoverToggle } from '@vben/hooks'; | |||
| import { LockKeyhole, LogOut } from '@vben/icons'; | ||||
| import { $t } from '@vben/locales'; | ||||
| import { preferences, usePreferences } from '@vben/preferences'; | ||||
| import { useLockStore } from '@vben/stores'; | ||||
| import { useAccessStore } from '@vben/stores'; | ||||
| import { isWindowsOs } from '@vben/utils'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben-core/popup-ui'; | ||||
|  | @ -82,7 +82,7 @@ const emit = defineEmits<{ logout: [] }>(); | |||
| 
 | ||||
| const { globalLockScreenShortcutKey, globalLogoutShortcutKey } = | ||||
|   usePreferences(); | ||||
| const lockStore = useLockStore(); | ||||
| const accessStore = useAccessStore(); | ||||
| const [LockModal, lockModalApi] = useVbenModal({ | ||||
|   connectedComponent: LockScreenModal, | ||||
| }); | ||||
|  | @ -133,7 +133,7 @@ function handleOpenLock() { | |||
| 
 | ||||
| function handleSubmitLock(lockScreenPassword: string) { | ||||
|   lockModalApi.close(); | ||||
|   lockStore.lockScreen(lockScreenPassword); | ||||
|   accessStore.lockScreen(lockScreenPassword); | ||||
| } | ||||
| 
 | ||||
| function handleLogout() { | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ | |||
|     "@vben-core/typings": "workspace:*", | ||||
|     "pinia": "catalog:", | ||||
|     "pinia-plugin-persistedstate": "catalog:", | ||||
|     "secure-ls": "catalog:", | ||||
|     "vue": "catalog:", | ||||
|     "vue-router": "catalog:" | ||||
|   } | ||||
|  |  | |||
|  | @ -27,6 +27,14 @@ interface AccessState { | |||
|    * 是否已经检查过权限 | ||||
|    */ | ||||
|   isAccessChecked: boolean; | ||||
|   /** | ||||
|    * 是否锁屏状态 | ||||
|    */ | ||||
|   isLockScreen: boolean; | ||||
|   /** | ||||
|    * 锁屏密码 | ||||
|    */ | ||||
|   lockScreenPassword?: string; | ||||
|   /** | ||||
|    * 登录是否过期 | ||||
|    */ | ||||
|  | @ -61,6 +69,10 @@ export const useAccessStore = defineStore('core-access', { | |||
|       } | ||||
|       return findMenu(this.accessMenus, path); | ||||
|     }, | ||||
|     lockScreen(password: string) { | ||||
|       this.isLockScreen = true; | ||||
|       this.lockScreenPassword = password; | ||||
|     }, | ||||
|     setAccessCodes(codes: string[]) { | ||||
|       this.accessCodes = codes; | ||||
|     }, | ||||
|  | @ -82,10 +94,20 @@ export const useAccessStore = defineStore('core-access', { | |||
|     setRefreshToken(token: AccessToken) { | ||||
|       this.refreshToken = token; | ||||
|     }, | ||||
|     unlockScreen() { | ||||
|       this.isLockScreen = false; | ||||
|       this.lockScreenPassword = undefined; | ||||
|     }, | ||||
|   }, | ||||
|   persist: { | ||||
|     // 持久化
 | ||||
|     pick: ['accessToken', 'refreshToken', 'accessCodes'], | ||||
|     pick: [ | ||||
|       'accessToken', | ||||
|       'refreshToken', | ||||
|       'accessCodes', | ||||
|       'isLockScreen', | ||||
|       'lockScreenPassword', | ||||
|     ], | ||||
|   }, | ||||
|   state: (): AccessState => ({ | ||||
|     accessCodes: [], | ||||
|  | @ -93,6 +115,8 @@ export const useAccessStore = defineStore('core-access', { | |||
|     accessRoutes: [], | ||||
|     accessToken: null, | ||||
|     isAccessChecked: false, | ||||
|     isLockScreen: false, | ||||
|     lockScreenPassword: undefined, | ||||
|     loginExpired: false, | ||||
|     refreshToken: null, | ||||
|   }), | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| export * from './access'; | ||||
| export * from './lock'; | ||||
| export * from './tabbar'; | ||||
| export * from './user'; | ||||
|  |  | |||
|  | @ -1,31 +0,0 @@ | |||
| import { createPinia, setActivePinia } from 'pinia'; | ||||
| import { beforeEach, describe, expect, it } from 'vitest'; | ||||
| 
 | ||||
| import { useLockStore } from './lock'; | ||||
| 
 | ||||
| describe('useLockStore', () => { | ||||
|   beforeEach(() => { | ||||
|     setActivePinia(createPinia()); | ||||
|   }); | ||||
| 
 | ||||
|   it('should initialize with correct default state', () => { | ||||
|     const store = useLockStore(); | ||||
|     expect(store.isLockScreen).toBe(false); | ||||
|     expect(store.lockScreenPassword).toBeUndefined(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should lock screen with a password', () => { | ||||
|     const store = useLockStore(); | ||||
|     store.lockScreen('1234'); | ||||
|     expect(store.isLockScreen).toBe(true); | ||||
|     expect(store.lockScreenPassword).toBe('1234'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should unlock screen and clear password', () => { | ||||
|     const store = useLockStore(); | ||||
|     store.lockScreen('1234'); | ||||
|     store.unlockScreen(); | ||||
|     expect(store.isLockScreen).toBe(false); | ||||
|     expect(store.lockScreenPassword).toBeUndefined(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,33 +0,0 @@ | |||
| import { defineStore } from 'pinia'; | ||||
| 
 | ||||
| interface AppState { | ||||
|   /** | ||||
|    * 是否锁屏状态 | ||||
|    */ | ||||
|   isLockScreen: boolean; | ||||
|   /** | ||||
|    * 锁屏密码 | ||||
|    */ | ||||
|   lockScreenPassword?: string; | ||||
| } | ||||
| 
 | ||||
| export const useLockStore = defineStore('core-lock', { | ||||
|   actions: { | ||||
|     lockScreen(password: string) { | ||||
|       this.isLockScreen = true; | ||||
|       this.lockScreenPassword = password; | ||||
|     }, | ||||
| 
 | ||||
|     unlockScreen() { | ||||
|       this.isLockScreen = false; | ||||
|       this.lockScreenPassword = undefined; | ||||
|     }, | ||||
|   }, | ||||
|   persist: { | ||||
|     pick: ['isLockScreen', 'lockScreenPassword'], | ||||
|   }, | ||||
|   state: (): AppState => ({ | ||||
|     isLockScreen: false, | ||||
|     lockScreenPassword: undefined, | ||||
|   }), | ||||
| }); | ||||
|  | @ -3,6 +3,7 @@ import type { Pinia } from 'pinia'; | |||
| import type { App } from 'vue'; | ||||
| 
 | ||||
| import { createPinia } from 'pinia'; | ||||
| import SecureLS from 'secure-ls'; | ||||
| 
 | ||||
| let pinia: Pinia; | ||||
| 
 | ||||
|  | @ -20,11 +21,27 @@ export async function initStores(app: App, options: InitStoreOptions) { | |||
|   const { createPersistedState } = await import('pinia-plugin-persistedstate'); | ||||
|   pinia = createPinia(); | ||||
|   const { namespace } = options; | ||||
|   const ls = new SecureLS({ | ||||
|     encodingType: 'aes', | ||||
|     encryptionSecret: import.meta.env.VITE_APP_STORE_SECURE_KEY, | ||||
|     isCompression: true, | ||||
|     // @ts-ignore secure-ls does not have a type definition for this
 | ||||
|     metaKey: `${namespace}-secure-meta`, | ||||
|   }); | ||||
|   pinia.use( | ||||
|     createPersistedState({ | ||||
|       // key $appName-$store.id
 | ||||
|       key: (storeKey) => `${namespace}-${storeKey}`, | ||||
|       storage: localStorage, | ||||
|       storage: import.meta.env.DEV | ||||
|         ? localStorage | ||||
|         : { | ||||
|             getItem(key) { | ||||
|               return ls.get(key); | ||||
|             }, | ||||
|             setItem(key, value) { | ||||
|               ls.set(key, value); | ||||
|             }, | ||||
|           }, | ||||
|     }), | ||||
|   ); | ||||
|   app.use(pinia); | ||||
|  |  | |||
|  | @ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin | |||
| 
 | ||||
| # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 | ||||
| VITE_APP_NAMESPACE=vben-web-play | ||||
| 
 | ||||
| # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 | ||||
| VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key | ||||
|  |  | |||
|  | @ -393,6 +393,9 @@ catalogs: | |||
|     sass: | ||||
|       specifier: ^1.86.3 | ||||
|       version: 1.86.3 | ||||
|     secure-ls: | ||||
|       specifier: ^2.0.0 | ||||
|       version: 2.0.0 | ||||
|     sortablejs: | ||||
|       specifier: ^1.15.6 | ||||
|       version: 1.15.6 | ||||
|  | @ -1778,6 +1781,9 @@ importers: | |||
|       pinia-plugin-persistedstate: | ||||
|         specifier: 'catalog:' | ||||
|         version: 4.2.0(magicast@0.3.5)(pinia@2.3.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))) | ||||
|       secure-ls: | ||||
|         specifier: 'catalog:' | ||||
|         version: 2.0.0 | ||||
|       vue: | ||||
|         specifier: ^3.5.13 | ||||
|         version: 3.5.13(typescript@5.8.3) | ||||
|  | @ -5666,6 +5672,9 @@ packages: | |||
|   crossws@0.3.4: | ||||
|     resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} | ||||
| 
 | ||||
|   crypto-js@4.2.0: | ||||
|     resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} | ||||
| 
 | ||||
|   crypto-random-string@2.0.0: | ||||
|     resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} | ||||
|     engines: {node: '>=8'} | ||||
|  | @ -7666,6 +7675,10 @@ packages: | |||
|     peerDependencies: | ||||
|       vue: ^3.5.13 | ||||
| 
 | ||||
|   lz-string@1.5.0: | ||||
|     resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} | ||||
|     hasBin: true | ||||
| 
 | ||||
|   magic-string@0.25.9: | ||||
|     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} | ||||
| 
 | ||||
|  | @ -9231,6 +9244,10 @@ packages: | |||
|     resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} | ||||
|     engines: {node: '>=4'} | ||||
| 
 | ||||
|   secure-ls@2.0.0: | ||||
|     resolution: {integrity: sha512-Wgtnw0QSm0v7gVKv11nOoeyGS65EThGXnBB7jfd4IhZd2eq3B4AMPcXAL5qJ1h55+Qolun7TONTwX7H5m6e2pQ==} | ||||
|     engines: {node: '>=8.0'} | ||||
| 
 | ||||
|   seemly@0.3.10: | ||||
|     resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==} | ||||
| 
 | ||||
|  | @ -14938,6 +14955,8 @@ snapshots: | |||
|     dependencies: | ||||
|       uncrypto: 0.1.3 | ||||
| 
 | ||||
|   crypto-js@4.2.0: {} | ||||
| 
 | ||||
|   crypto-random-string@2.0.0: {} | ||||
| 
 | ||||
|   cspell-config-lib@8.18.1: | ||||
|  | @ -17132,6 +17151,8 @@ snapshots: | |||
|     dependencies: | ||||
|       vue: 3.5.13(typescript@5.8.3) | ||||
| 
 | ||||
|   lz-string@1.5.0: {} | ||||
| 
 | ||||
|   magic-string@0.25.9: | ||||
|     dependencies: | ||||
|       sourcemap-codec: 1.4.8 | ||||
|  | @ -18814,6 +18835,11 @@ snapshots: | |||
|       extend-shallow: 2.0.1 | ||||
|       kind-of: 6.0.3 | ||||
| 
 | ||||
|   secure-ls@2.0.0: | ||||
|     dependencies: | ||||
|       crypto-js: 4.2.0 | ||||
|       lz-string: 1.5.0 | ||||
| 
 | ||||
|   seemly@0.3.10: {} | ||||
| 
 | ||||
|   select@1.1.2: {} | ||||
|  |  | |||
|  | @ -147,6 +147,7 @@ catalog: | |||
|   rollup: ^4.39.0 | ||||
|   rollup-plugin-visualizer: ^5.14.0 | ||||
|   sass: ^1.86.3 | ||||
|   secure-ls: ^2.0.0 | ||||
|   sortablejs: ^1.15.6 | ||||
|   stylelint: ^16.18.0 | ||||
|   stylelint-config-recess-order: ^5.1.1 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Netfan
						Netfan