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