Svelte集成Keycloak:免刷新按Token权限加载数据方案求助
解决Svelte中Keycloak初始化与服务器端数据加载时序问题
问题背景
在Svelte项目中集成Keycloak,所有相关逻辑封装在keycloakInteraction.ts中复用。需求是:初始化Keycloak实例(会话失效或不存在时引导用户登录),并根据Token中的user_group属性加载对应权限的数据。
当前核心问题:+layout.server.ts的执行时机早于+layout.svelte中的Keycloak初始化流程,导致Token未就绪时返回空数据。临时方案是登录后刷新页面,但Token过期重登后无法自动触发刷新,需要无需刷新页面的优雅解决方案。
现有代码片段
+layout.svelte
<script> import '../app.css'; import { page } from '$app/stores'; import { onMount } from 'svelte'; import { initKeycloakOnClient, keycloakClientTokenStoreOnClient, } from '$lib/keycloakInteraction'; export let data; $: keycloakState = $keycloakClientTokenStoreOnClient; $: isAuthenticated = keycloakState.authenticated; onMount(async () => { await initKeycloakOnClient(); hasReloaded = sessionStorage.getItem("hasReloadedAfterLogin"); if (isAuthenticated && !hasReloaded) { console.log("User just logged in, reloading the page..."); sessionStorage.setItem("hasReloadedAfterLogin", "true"); location.reload(); } }); </script> {#if isAuthenticated} <!--MENU HTML CODE IS HERE--> {#each data.routes as route} {/each} <!--...and other data used provided by +layout.server.ts--> <slot /> {:else} <div class="h-screen flex justify-center items-center"> <p>Loading...</p> </div> {/if}
+layout.server.ts
import type { LayoutServerLoad } from './$types'; import { selectAllClients, selectAllEngagements } from '$lib/server/database/queries'; import { validateClientTokenOnServer, resetKeycloakClientTokenStore } from '$lib/keycloakInteractionResource'; import { keycloak } from '$lib/keycloakInteraction'; const routes = [ { name: 'Link#1', }, { name: 'Link#2', } ]; export const load: LayoutServerLoad = async ( request ) => { let userGroup: string = null; //getting clientToken from server-side cookies that were put by storeKeycloakTokenOnServer() const clientToken = request.cookies.get('client_token'); if (clientToken) { await validateClientTokenOnServer(clientToken); const decodedToken = JSON.parse(atob(clientToken.split(".")[1])); userGroup = decodedToken?.user_groups || []; const availableData = await selectAllData(userGroup); const availableRoutes = userGroup.includes("testGroup") ? routes.filter(route => route.name !== 'Link#2') : routes; await resetKeycloakClientTokenStore(); return { data: availableData, routes: availableRoutes } } }
keycloakInteraction.ts
import Keycloak from 'keycloak-js'; import { writable } from 'svelte/store'; const keycloakUrl = import.meta.env.VITE_AUTH_SERVICE_URL; const keycloakRealm = import.meta.env.VITE_AUTH_SERVICE_REALM; const keycloakClientId = import.meta.env.VITE_AUTH_SERVICE_CLIENT_ID; const keycloak = new Keycloak({ url: keycloakUrl, realm: keycloakRealm, clientId: keycloakClientId, }); export const keycloakClientTokenStoreOnClient = writable({ authenticated: false, token: null, userGroups: [], firstName: null, lastName: null }); export const initKeycloakOnClient = async () => { try { await keycloak.init({ checkLoginIframe: false, onLoad: 'login-required', }); if (keycloak.authenticated) { await updateKeycloakStoreOnClient(); // performs keycloakClientTokenStoreOnClient.set ... await storeKeycloakTokenOnServer(keycloak.token); } } catch (error) { console.error('Keycloak initialization failed:', error); }
解决方案
方案1:客户端触发服务器端数据重载(无需页面刷新)
核心逻辑:Keycloak初始化完成并获取Token后,调用SvelteKit的invalidateAll方法触发服务器端重新执行load函数,替代页面刷新。
- 修改
+layout.svelte,移除刷新逻辑:
<script> import '../app.css'; import { page, invalidateAll } from '$app/stores'; import { onMount } from 'svelte'; import { initKeycloakOnClient, keycloakClientTokenStoreOnClient, } from '$lib/keycloakInteraction'; export let data; $: keycloakState = $keycloakClientTokenStoreOnClient; $: isAuthenticated = keycloakState.authenticated; onMount(async () => { await initKeycloakOnClient(); // 认证完成后触发服务器端数据重载 if (isAuthenticated) await invalidateAll(); }); </script> {#if isAuthenticated} <!--MENU HTML CODE IS HERE--> {#each data?.routes || [] as route} <!-- 渲染路由内容 --> {/each} <slot /> {:else} <div class="h-screen flex justify-center items-center"> <p>Loading...</p> </div> {/if}
- 优化
+layout.server.ts,处理Token为空的默认情况:
export const load: LayoutServerLoad = async ( request ) => { let userGroup: string[] = []; const clientToken = request.cookies.get('client_token'); if (clientToken) { await validateClientTokenOnServer(clientToken); const decodedToken = JSON.parse(atob(clientToken.split(".")[1])); userGroup = decodedToken?.user_groups || []; } const availableData = await selectAllData(userGroup); const availableRoutes = userGroup.includes("testGroup") ? routes.filter(route => route.name !== 'Link#2') : routes; await resetKeycloakClientTokenStore(); return { data: availableData, routes: availableRoutes } }
方案2:服务器端前置Keycloak认证(推荐)
核心逻辑:利用SvelteKit的hooks.server.ts在页面加载前完成Token验证,确保+layout.server.ts执行时Token已就绪,从根源解决时序问题。
- 创建
hooks.server.ts处理服务器端认证:
import type { Handle } from '@sveltejs/kit'; import { keycloak } from '$lib/keycloakInteraction'; export const handle: Handle = async ({ event, resolve }) => { const clientToken = event.cookies.get('client_token'); if (!clientToken) { // 无Token时直接跳转到Keycloak登录页 return new Response(null, { status: 302, headers: { Location: keycloak.createLoginUrl() } }); } try { // 验证Token有效性 const isValid = await keycloak.validateToken(clientToken); if (!isValid) { event.cookies.delete('client_token'); return new Response(null, { status: 302, headers: { Location: keycloak.createLoginUrl() } }); } } catch (error) { console.error('Token validation failed:', error); event.cookies.delete('client_token'); return new Response(null, { status: 302, headers: { Location: keycloak.createLoginUrl() } }); } return resolve(event); };
- 简化
+layout.server.ts,此时Token已确保有效:
export const load: LayoutServerLoad = async ( request ) => { const clientToken = request.cookies.get('client_token'); const decodedToken = JSON.parse(atob(clientToken.split(".")[1])); const userGroup = decodedToken?.user_groups || []; const availableData = await selectAllData(userGroup); const availableRoutes = userGroup.includes("testGroup") ? routes.filter(route => route.name !== 'Link#2') : routes; return { data: availableData, routes: availableRoutes } }
- 简化
+layout.svelte,无需处理刷新逻辑:
<script> import '../app.css'; import { keycloakClientTokenStoreOnClient } from '$lib/keycloakInteraction'; export let data; $: isAuthenticated = $keycloakClientTokenStoreOnClient.authenticated; </script> {#if isAuthenticated} <!--MENU HTML CODE IS HERE--> {#each data.routes as route} <!-- 渲染路由内容 --> {/each} <slot /> {:else} <div class="h-screen flex justify-center items-center"> <p>Loading...</p> </div> {/if}
方案3:客户端直接过滤数据(适用于非敏感权限逻辑)
如果权限逻辑不涉及敏感数据,可以直接在客户端根据Keycloak Store中的userGroups过滤数据,无需依赖服务器端:
<script> import '../app.css'; import { onMount } from 'svelte'; import { initKeycloakOnClient, keycloakClientTokenStoreOnClient, } from '$lib/keycloakInteraction'; const allRoutes = [ { name: 'Link#1' }, { name: 'Link#2' } ]; $: keycloakState = $keycloakClientTokenStoreOnClient; $: isAuthenticated = keycloakState.authenticated; $: availableRoutes = isAuthenticated && keycloakState.userGroups.includes("testGroup") ? allRoutes.filter(r => r.name !== 'Link#2') : allRoutes; onMount(async () => { await initKeycloakOnClient(); }); </script> {#if isAuthenticated} <!--MENU HTML CODE IS HERE--> {#each availableRoutes as route} <!-- 渲染路由内容 --> {/each} <slot /> {:else} <div class="h-screen flex justify-center items-center"> <p>Loading...</p> </div> {/if}
内容的提问来源于stack exchange,提问作者forkintheass
相关产品推荐
相关产品推荐

