Svelte 鉴权守卫(auth-guard)组件实现与优化

1. 需求背景

在前端项目中,常常需要对部分页面进行访问控制(如:未登录用户不能访问某些页面,未完善资料的用户需跳转到资料完善页)。为此,我们实现了一个 Svelte 组件 auth-guard,用于统一处理页面的鉴权与重定向逻辑。


2. 主要思路

  • 区分公开页面与受保护页面:如首页 /、登录页 /login 为公开页面,其余页面需登录后访问。
  • 未登录时自动跳转到登录页
  • 已登录但未设置昵称时,强制跳转到设置昵称页 /set-uname
  • 所有鉴权逻辑在 slot 内容渲染前完成,避免未授权内容闪现

3. 关键实现

3.1 公开页面路径配置

const publicPaths = ['/', '/login'];
const isPublicPath = (path: string) => publicPaths.includes(path);

3.2 鉴权检查函数

const checkAuth = () => {
  if (!browser) return;
  const currentPath = page.url.pathname;
  if (isPublicPath(currentPath)) return;
  if (!$auth.isAuthenticated) {
    goto('/login', { replaceState: true });
    return;
  }
  if ($auth.isAuthenticated && (!$user.nickname || $user.nickname === '') && currentPath !== '/set-uname') {
    goto('/set-uname', { replaceState: true });
    return;
  }
};

3.3 响应式监听

利用 $effect 监听认证状态和路径变化,及时执行鉴权逻辑:

$effect(() => {
  if ((browser && !$auth.isAuthenticated) || page.url.pathname) {
    checkAuth();
  }
});

3.4 渲染控制

通过 Svelte 的条件渲染,确保 slot 只在鉴权通过时才渲染:

{#if $auth.isLoading || $user.isLoading}
  <!-- 加载状态 -->
{:else if isPublicPath(page.url.pathname)}
  <slot />
{:else if $auth.isAuthenticated && ($user.nickname || page.url.pathname === '/set-uname')}
  <slot />
{:else}
  <div></div> <!-- 鉴权中或重定向中占位 -->
{/if}

4. 常见问题与优化

问题:slot 内容提前渲染导致未授权内容闪现

原因:如果在 onMount 里执行鉴权,Svelte 会先渲染 slot,再执行 onMount,导致未授权内容短暂可见。

优化:去掉 onMount 里的 checkAuth(),只用 $effect 响应式监听,并在 slot 渲染前加条件判断。

问题:首页(/)或公开页面因未设置昵称无法访问

原因:slot 渲染条件未考虑公开页面,导致首页等页面在未设置昵称时也不渲染。

优化:在渲染条件中加入 isPublicPath(page.url.pathname),公开页面直接渲染 slot。


5. 总结

  • 鉴权逻辑应在 slot 渲染前完成,避免未授权内容闪现。
  • 公开页面应单独处理,避免被鉴权逻辑误伤。
  • 利用 Svelte 的响应式和条件渲染,可以优雅地实现前端页面的访问控制。

如果 Svelte 要是像vue一样,有路由守卫,就不用这么麻烦了!

贴上完整代码

<script lang="ts">
  import { goto } from '$app/navigation';
  import { page } from '$app/state';
  import { browser } from '$app/environment';
  import { auth, me } from '$lib/stores';
  import { onMount } from 'svelte';

  let { children } = $props();

  // 检查当传入路径是否需要登录(默认检查当前路径)
  const checkPublicPath = (path: string) => {
    // 当前路径(不要写默认的,必须用地方传,否则会失去响应式)
    // const currentPath = page.url.pathname;
    // 不需要登录的页面路径
    const publicPaths = ['/', '/login'];
    return publicPaths.includes(path);
  };

  // 检查是否需要重定向
  const checkAuth = () => {
    console.log('执行了');

    // 只在客户端执行重定向逻辑
    if (!browser) return;

    // 如果当前路径不需要登录,直接显示
    if (checkPublicPath(page.url.pathname)) {
      return;
    }

    // 如果未登录且当前路径需要登录,直接重定向到登录页
    if (!$auth.isAuthenticated) {
      goto('/login', { replaceState: true });
      return;
    }

    // 如果已登录但未设置用户名,跳转到set-uname
    if (!$me.username && page.url.pathname !== '/set-uname') {
      goto('/set-uname', { replaceState: true });
      return;
    }

    // 如果在 /set-uname 页面,且已经有 username,跳转到 /home
    if (page.url.pathname === '/set-uname' && $me.username) {
      goto('/home', { replaceState: true });
      return;
    }
  };

  let mounted = false;
  onMount(async () => {
    if ($auth.isAuthenticated && !checkPublicPath(page.url.pathname)) {
      await me.sync();
    }
    // 后续这里不执行,交给effect监听路由变化去做
    if(!mounted){
      checkAuth();
      console.log('首次');
    }
   
    mounted = true;
  });

  // 监听认证状态和路径变化
  $effect(() => {
    // 只在需要登录的页面才执行 checkAuth
    if (!checkPublicPath(page.url.pathname) && mounted) {
      checkAuth();
    }
  });
</script>

{#if $auth.isLoading || $me.isLoading}
  <!-- 加载状态 -->
  <div class="min-h-screen flex items-center justify-center bg-gray-50">
    <div class="text-center">
      <div class="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
      <p class="text-gray-600">加载中...</p>
    </div>
  </div>
  <!-- 不需要权限的正常显示页面内容 -->
{:else if checkPublicPath(page.url.pathname)}
  <!-- <slot /> -->
  {@render children()}
  <!-- 需要权限的页面内容展示时机:需要等待鉴权完成 && 存在用户名字段或者正在设置用户名页面(如果不判断路由则会导致先到其它路由再回set-uname) -->
{:else if $auth.isAuthenticated && ($me.username || page.url.pathname === '/set-uname')}
  <!-- <slot /> -->
  {@render children()}
  <!-- {:else}
  <div>鉴权中...</div> -->
{/if}
posted @ 2025-07-22 15:08  丁少华  阅读(11)  评论(0)    收藏  举报