You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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函数,替代页面刷新。

  1. 修改+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}
  1. 优化+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已就绪,从根源解决时序问题。

  1. 创建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);
};
  1. 简化+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
    }
}
  1. 简化+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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:22:05