diff --git a/README.md b/README.md index 2de79a2..6d69a70 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ [**Demo演示**](https://2fa.nezha.pp.ua) https://2fa.nezha.pp.ua -- 设置任何用户都能登录,不受登录白名单限制,正常部署,只有设置的白名单用户才能登录 -- 演示数据可以任意修改、删除; + +- 演示站默认允许任何用户登录,区别于正常部署只有白名单用户才允许登录 +- 演示站数据可以任意修改、删除 --- @@ -162,7 +163,7 @@ cd backend npx wrangler d1 execute 2fauth-db-dev --local --env dev --file=schema.sql # 4. 终端启动应用 -# 终端 1: +```bash cd backend && npm run dev ``` diff --git a/backend/src/features/auth/authRoutes.ts b/backend/src/features/auth/authRoutes.ts index 4e8a573..189ba1d 100644 --- a/backend/src/features/auth/authRoutes.ts +++ b/backend/src/features/auth/authRoutes.ts @@ -48,7 +48,7 @@ auth.post('/callback/:provider', async (c) => { const service = getService(c); // 调用 Service 层处理登录 - const { token, userInfo } = await service.handleOAuthCallback(providerName, body); + const { token, userInfo, deviceKey } = await service.handleOAuthCallback(providerName, body); // 1. 设置 httpOnly 的鉴权 Cookie setCookie(c, 'auth_token', token, { @@ -71,7 +71,8 @@ auth.post('/callback/:provider', async (c) => { return c.json({ success: true, - userInfo + userInfo, + deviceKey }); }); diff --git a/backend/src/features/auth/authService.ts b/backend/src/features/auth/authService.ts index 51f0047..0a9f7c0 100644 --- a/backend/src/features/auth/authService.ts +++ b/backend/src/features/auth/authService.ts @@ -26,9 +26,9 @@ export class AuthService { } /** - * OAuth Callback 处理,进行鉴权并签发系统内部 JWT token + * OAuth Callback 处理,生成会话并返回附加参数 */ - async handleOAuthCallback(providerName: string, body: any): Promise<{ token: string, userInfo: OAuthUserInfo }> { + async handleOAuthCallback(providerName: string, body: any): Promise<{ token: string, userInfo: OAuthUserInfo, deviceKey: string }> { const provider = getOAuthProvider(providerName, this.env); let params: string | URLSearchParams; @@ -53,7 +53,27 @@ export class AuthService { // 签发 Token const token = await this.generateSystemToken(userInfo); - return { token, userInfo }; + // 附加客户端签名因子 + const deviceKey = await this.generateDeviceKey(userInfo.id); + + return { token, userInfo, deviceKey }; + } + + /** + * 生成客户端绑定标识 + */ + private async generateDeviceKey(userId: string): Promise { + const secret = this.env.JWT_SECRET; + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign('HMAC', keyMaterial, encoder.encode(userId + "device_salt_offline_key")); + return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join(''); } /** diff --git a/frontend/src/features/auth/store/userStore.js b/frontend/src/features/auth/store/userStore.js index b29317f..803dba2 100644 --- a/frontend/src/features/auth/store/userStore.js +++ b/frontend/src/features/auth/store/userStore.js @@ -2,6 +2,7 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { request } from '@/shared/utils/request' import { useVaultStore } from '@/features/vault/store/vaultStore' +import { removeIdbItem } from '@/shared/utils/idb' export const useUserStore = defineStore('user', () => { const getStoredUser = () => { @@ -19,13 +20,19 @@ export const useUserStore = defineStore('user', () => { localStorage.setItem('userInfo', JSON.stringify(info)) } - const clearUserInfo = () => { + const clearUserInfo = async () => { userInfo.value = {} localStorage.removeItem('userInfo') localStorage.removeItem('secure_vault') localStorage.removeItem('backup_providers_cache') - // 锁定保险箱 + // 清理本地持久化运行配置 + try { + await removeIdbItem('device_salt') + } catch (e) { + console.error('Failed to remove device_salt from IDB', e) + } + const vaultStore = useVaultStore() vaultStore.lock() } @@ -36,7 +43,7 @@ export const useUserStore = defineStore('user', () => { } catch (e) { console.error('Logout request failed', e) } finally { - clearUserInfo() + await clearUserInfo() } } @@ -48,7 +55,7 @@ export const useUserStore = defineStore('user', () => { return true } } catch (e) { - clearUserInfo() + await clearUserInfo() return false } return false diff --git a/frontend/src/features/auth/views/oauthCallback.vue b/frontend/src/features/auth/views/oauthCallback.vue index b002eb9..c785ef5 100644 --- a/frontend/src/features/auth/views/oauthCallback.vue +++ b/frontend/src/features/auth/views/oauthCallback.vue @@ -31,6 +31,7 @@ import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Loading } from '@element-plus/icons-vue' import { useUserStore } from '@/features/auth/store/userStore' +import { setIdbItem } from '@/shared/utils/idb' const route = useRoute() const router = useRouter() @@ -125,6 +126,11 @@ onMounted(async () => { if (data.success) { // 登录成功!Token 已写入 httpOnly Cookie + // 持久化客户端参数 + if (data.deviceKey) { + await setIdbItem('device_salt', data.deviceKey) + } + // 立即调用接口确认会话有效性,并更新用户信息 const userStore = useUserStore() await userStore.fetchUserInfo() diff --git a/frontend/src/features/backup/components/backupSettings.vue b/frontend/src/features/backup/components/backupSettings.vue index cc38ccf..e957b6f 100644 --- a/frontend/src/features/backup/components/backupSettings.vue +++ b/frontend/src/features/backup/components/backupSettings.vue @@ -228,15 +228,11 @@ const isRestoring = ref(false) // --- API 交互 --- const fetchProviders = async () => { - // 1. 离线优先:尝试加载加密缓存 (仅在保险箱已解锁时有效) - if (vaultStore.isUnlocked) { - const cachedEncrypted = await vaultStore.getEncryptedBackupProviders() - if (cachedEncrypted && Array.isArray(cachedEncrypted)) { - providers.value = cachedEncrypted - // 有缓存时静默更新,不转圈圈 - } else { - isLoading.value = true - } + // 1. 离线优先:尝试加载加密缓存 + const cachedEncrypted = await vaultStore.getEncryptedBackupProviders() + if (cachedEncrypted && Array.isArray(cachedEncrypted)) { + providers.value = cachedEncrypted + // 有缓存时静默更新,不转圈圈 } else { isLoading.value = true } @@ -245,10 +241,8 @@ const fetchProviders = async () => { const res = await request('/api/backups/providers') if (res.success) { providers.value = res.providers - // 2. 更新加密缓存 (如果保险箱已解锁) - if (vaultStore.isUnlocked) { - await vaultStore.saveEncryptedBackupProviders(res.providers) - } + // 2. 更新加密缓存 + await vaultStore.saveEncryptedBackupProviders(res.providers) } } finally { isLoading.value = false } } diff --git a/frontend/src/features/vault/components/vaultList.vue b/frontend/src/features/vault/components/vaultList.vue index e181131..5dc6628 100644 --- a/frontend/src/features/vault/components/vaultList.vue +++ b/frontend/src/features/vault/components/vaultList.vue @@ -1,12 +1,7 @@