MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SvelteKit 路由守卫:实现页面访问控制与权限管理

2022-12-243.8k 阅读

SvelteKit 路由守卫基础概念

什么是路由守卫

在前端应用开发中,路由守卫是一种机制,用于在路由导航发生时,对用户的访问进行控制。它允许开发者决定用户是否能够访问特定的页面。这在实现权限管理、认证校验等功能时非常关键。例如,只有登录用户才能访问个人资料页面,未登录用户则应被重定向到登录页面。在 SvelteKit 中,路由守卫同样扮演着这样的角色,它帮助我们在路由层面实现页面访问控制和权限管理。

SvelteKit 路由机制简介

SvelteKit 的路由基于文件系统。在项目的 src/routes 目录下,每个文件和目录都对应一个路由。例如,src/routes/about.svelte 对应 /about 路由,src/routes/blog/[slug].svelte 对应动态路由,其中 [slug] 是动态参数。这种基于文件系统的路由方式使得路由的管理非常直观和便捷。

实现简单的路由守卫

创建基本的路由守卫函数

我们可以在 SvelteKit 项目中创建一个路由守卫函数。假设我们有一个简单的需求:只有用户登录了才能访问特定页面。我们首先创建一个用于判断用户是否登录的函数,例如:

// auth.js
export const isLoggedIn = () => {
    // 这里假设通过检查 localStorage 中的 token 来判断用户是否登录
    return localStorage.getItem('token')!== null;
};

然后,我们创建一个路由守卫函数,这个函数将在路由导航前被调用:

// routeGuard.js
import { isLoggedIn } from './auth.js';

export const protectRoute = (event) => {
    if (!isLoggedIn()) {
        // 如果用户未登录,重定向到登录页面
        throw {
            status: 302,
            location: '/login'
        };
    }
};

在上述代码中,protectRoute 函数首先调用 isLoggedIn 函数判断用户是否登录。如果用户未登录,它抛出一个包含 statuslocation 的对象。status 设置为 302,表示重定向,location 则指定重定向到 /login 页面。

在 SvelteKit 路由中使用路由守卫

在 SvelteKit 中,我们可以在路由文件中使用这个路由守卫。假设我们有一个需要登录才能访问的 src/routes/dashboard.svelte 文件,我们可以这样修改:

<script context="module">
    import { protectRoute } from '../routeGuard.js';

    export const load = async (event) => {
        protectRoute(event);
        // 这里可以继续加载页面所需的数据
        return {
            message: 'Welcome to the dashboard!'
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

在上述代码中,我们在 load 函数中调用了 protectRoute 路由守卫。load 函数是 SvelteKit 提供的用于加载页面数据的函数,在页面渲染前会被调用。如果用户未登录,protectRoute 函数会抛出重定向信息,页面将不会渲染,而是重定向到登录页面。

权限管理中的路由守卫

基于角色的权限管理

除了简单的登录状态判断,在实际应用中,我们常常需要基于用户角色进行权限管理。例如,管理员角色可以访问所有页面,而普通用户只能访问部分页面。首先,我们需要在用户登录时获取用户的角色信息,并存储起来,假设我们将角色信息存储在 localStorage 中:

// auth.js
export const getRole = () => {
    return localStorage.getItem('role');
};

然后,我们创建一个基于角色的路由守卫函数:

// roleRouteGuard.js
import { getRole } from './auth.js';

export const adminOnly = (event) => {
    const role = getRole();
    if (role!== 'admin') {
        throw {
            status: 403,
            body: 'Forbidden'
        };
    }
};

在上述代码中,adminOnly 函数获取用户角色,如果角色不是 admin,则抛出一个 403 Forbidden 错误。

在路由中应用基于角色的路由守卫

假设我们有一个 src/routes/admin/settings.svelte 文件,只有管理员能访问,我们可以这样应用路由守卫:

<script context="module">
    import { adminOnly } from '../roleRouteGuard.js';

    export const load = async (event) => {
        adminOnly(event);
        // 这里可以继续加载管理员设置页面所需的数据
        return {
            message: 'Admin settings page'
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

当普通用户尝试访问 /admin/settings 页面时,由于 adminOnly 路由守卫的作用,会返回一个 403 Forbidden 错误。

全局路由守卫

创建全局路由守卫

在一些情况下,我们希望对整个应用的路由都应用某些守卫逻辑,例如记录页面访问日志。我们可以通过在 src/hooks.js 文件中实现全局路由守卫。首先,创建一个简单的日志记录函数:

// logger.js
export const logPageVisit = (url) => {
    console.log(`Visited page: ${url}`);
};

然后,在 src/hooks.js 文件中添加全局路由守卫:

import { logPageVisit } from './logger.js';

export const handle = async ({ event, resolve }) => {
    logPageVisit(event.url.pathname);
    const response = await resolve(event);
    return response;
};

在上述代码中,handle 函数是 SvelteKit 提供的全局请求处理函数。每次路由导航时,它都会先调用 logPageVisit 函数记录页面访问日志,然后再调用 resolve 函数继续处理路由请求。

结合全局与局部路由守卫

我们可以将全局路由守卫与局部路由守卫结合使用。例如,我们在 src/routes/blog/[slug].svelte 中既有基于登录状态的局部路由守卫,又希望应用全局的页面访问日志记录:

<script context="module">
    import { protectRoute } from '../routeGuard.js';

    export const load = async (event) => {
        protectRoute(event);
        // 这里可以继续加载博客文章所需的数据
        return {
            message: 'Blog article'
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

在这个例子中,首先会触发全局路由守卫记录页面访问日志,然后在局部路由中,protectRoute 函数会判断用户是否登录,只有登录用户才能访问该博客文章页面。

处理异步路由守卫

异步路由守卫场景

在实际应用中,有些路由守卫逻辑可能是异步的。例如,我们可能需要从服务器获取用户的最新权限信息,而不是依赖本地存储的数据。假设我们有一个 API 来获取用户权限:

// api.js
import { browser } from '$app/environment';
import { get } from 'svelte/store';
import { getToken } from './auth.js';

export const fetchUserPermissions = async () => {
    if (!browser) return [];
    const token = getToken();
    const response = await fetch('/api/permissions', {
        headers: {
            Authorization: `Bearer ${token}`
        }
    });
    if (!response.ok) {
        throw new Error('Failed to fetch permissions');
    }
    return response.json();
};

实现异步路由守卫

我们基于上述异步获取权限的函数来创建异步路由守卫:

// asyncRouteGuard.js
import { fetchUserPermissions } from './api.js';

export const hasPermission = async (event, requiredPermission) => {
    const permissions = await fetchUserPermissions();
    if (!permissions.includes(requiredPermission)) {
        throw {
            status: 403,
            body: 'Forbidden'
        };
    }
};

在上述代码中,hasPermission 函数首先异步获取用户权限,然后检查用户是否具备所需的权限。如果没有,则抛出 403 Forbidden 错误。

在路由中使用异步路由守卫

假设我们有一个需要特定权限才能访问的 src/routes/special-report.svelte 文件:

<script context="module">
    import { hasPermission } from '../asyncRouteGuard.js';

    export const load = async (event) => {
        await hasPermission(event,'read_special_report');
        // 这里可以继续加载特殊报告页面所需的数据
        return {
            message: 'Special report page'
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

在上述代码中,load 函数等待 hasPermission 异步路由守卫完成权限检查后,才继续加载页面数据。如果用户没有 read_special_report 权限,将返回 403 Forbidden 错误。

嵌套路由中的路由守卫

嵌套路由结构

SvelteKit 支持嵌套路由。例如,我们有一个 src/routes/products 目录,其中 products.svelte 是产品列表页面,[productId] 目录下的 details.svelte 是产品详情页面,形成了嵌套路由结构。

src/routes/products
├── products.svelte
└── [productId]
    └── details.svelte

在嵌套路由中应用路由守卫

假设我们需要用户登录才能访问产品详情页面,我们可以在 src/routes/products/[productId]/details.svelte 中应用路由守卫:

<script context="module">
    import { protectRoute } from '../../../routeGuard.js';

    export const load = async (event) => {
        protectRoute(event);
        const productId = event.params.productId;
        // 这里可以根据 productId 加载产品详情数据
        return {
            productId,
            message: `Product details for ${productId}`
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

在上述代码中,protectRoute 路由守卫确保只有登录用户才能访问产品详情页面。同时,通过 event.params.productId 获取动态路由参数,用于加载相应产品的详情数据。

嵌套路由守卫的继承与覆盖

在嵌套路由中,父路由的路由守卫可能会被子路由继承。例如,如果在 src/routes/products/products.svelte 中有一个全局的登录检查路由守卫,src/routes/products/[productId]/details.svelte 默认也会继承这个检查(因为都在 products 路由分支下)。但如果 details.svelte 有自己特殊的权限要求,也可以覆盖父路由的守卫逻辑。例如,除了登录,产品详情页面可能还要求用户具备 view_product_details 权限:

<script context="module">
    import { protectRoute } from '../../../routeGuard.js';
    import { hasPermission } from '../../../asyncRouteGuard.js';

    export const load = async (event) => {
        protectRoute(event);
        await hasPermission(event, 'view_product_details');
        const productId = event.params.productId;
        return {
            productId,
            message: `Product details for ${productId}`
        };
    };
</script>

<script>
    export let data;
</script>

<h1>{data.message}</h1>

在这个例子中,既应用了父路由的登录检查守卫 protectRoute,又添加了自己特殊的权限检查 hasPermission

路由守卫与页面过渡效果

结合路由守卫与页面过渡

SvelteKit 支持页面过渡效果,我们可以将路由守卫与页面过渡结合起来,提升用户体验。例如,当用户因为权限不足被重定向时,我们希望有一个平滑的过渡效果。首先,在 src/routes/__layout.svelte 中设置页面过渡:

<script>
    import { fade } from 'svelte/transition';
</script>

{#if $page}
    <div transition:fade>
        {#await $page}
            <p>Loading...</p>
        {:then page}
            {page}
        {:catch error}
            <p>{error.message}</p>
        {/await}
    </div>
{/if}

在上述代码中,我们使用了 Svelte 的 fade 过渡效果。当路由发生变化时,页面会有淡入淡出的过渡。

权限不足时的过渡处理

当用户因为权限不足被重定向时,我们希望过渡效果也能正常工作。假设用户尝试访问 /admin/settings 页面但权限不足,adminOnly 路由守卫抛出 403 Forbidden 错误,SvelteKit 会处理这个错误并展示相应的错误页面。由于我们在 __layout.svelte 中设置了过渡效果,错误页面的展示也会有淡入淡出的过渡,这样用户体验会更加流畅。

同时,我们可以在错误页面中添加一些提示信息,告知用户权限不足的原因:

<script context="module">
    export const load = async (event) => {
        try {
            // 这里假设已经有 adminOnly 路由守卫逻辑
            throw {
                status: 403,
                body: 'You do not have permission to access this page'
            };
        } catch (error) {
            return {
                error
            };
        }
    };
</script>

<script>
    export let data;
</script>

<h1>Error {data.error.status}</h1>
<p>{data.error.body}</p>

在上述代码中,load 函数故意抛出一个 403 错误模拟权限不足情况,并将错误信息传递给页面。页面展示错误状态码和错误原因,同时由于 __layout.svelte 的过渡设置,页面展示有淡入效果。

路由守卫的优化与注意事项

性能优化

在使用路由守卫时,性能是一个需要考虑的因素。特别是在异步路由守卫中,尽量减少不必要的异步操作。例如,如果在多个路由中都需要获取用户权限,我们可以考虑在用户登录时一次性获取并存储在本地,而不是每次路由导航都去服务器获取。这样可以减少网络请求,提高应用性能。

另外,对于全局路由守卫,避免在其中执行过于复杂的操作,因为它会在每次路由导航时被调用。如果有复杂的逻辑,可以考虑将其拆分成局部路由守卫,只在需要的路由中执行。

错误处理与用户反馈

在路由守卫中,要妥善处理错误并给用户提供清晰的反馈。例如,当用户权限不足时,除了返回 403 Forbidden 错误,还可以在错误页面中详细说明原因,如 “您没有权限访问此页面,请联系管理员获取权限”。同时,对于异步路由守卫中的网络错误等,也要有合适的提示,如 “获取权限信息失败,请稍后重试”。

测试路由守卫

为了确保路由守卫的正确性,需要对其进行测试。可以使用 Jest 等测试框架来测试路由守卫函数。例如,对于 isLoggedIn 函数,我们可以这样测试:

import { isLoggedIn } from './auth.js';

describe('isLoggedIn', () => {
    beforeEach(() => {
        localStorage.removeItem('token');
    });

    it('should return false when no token is present', () => {
        expect(isLoggedIn()).toBe(false);
    });

    it('should return true when token is present', () => {
        localStorage.setItem('token', 'valid_token');
        expect(isLoggedIn()).toBe(true);
    });
});

在上述测试中,我们使用 Jest 的 describeit 来定义测试用例。beforeEach 函数在每个测试用例执行前清除 localStorage 中的 token。然后分别测试没有 token 和有 tokenisLoggedIn 函数的返回值。

对于路由守卫函数,如 protectRoute,我们可以模拟 event 对象并测试其重定向逻辑:

import { protectRoute } from './routeGuard.js';

describe('protectRoute', () => {
    it('should throw a redirect when user is not logged in', () => {
        const mockEvent = {};
        expect(() => protectRoute(mockEvent)).toThrow();
    });
});

在这个测试中,我们模拟一个空的 event 对象,测试当用户未登录时 protectRoute 函数是否会抛出重定向信息。

通过以上对 SvelteKit 路由守卫的详细介绍,从基础概念到各种应用场景,以及优化与注意事项,相信开发者能够在前端项目中更好地利用路由守卫实现页面访问控制与权限管理,打造出更安全、用户体验更好的应用。