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

React 路由守卫与权限控制的实现

2021-09-024.8k 阅读

React 路由守卫的基本概念

在 React 应用开发中,路由守卫起着至关重要的作用。它允许我们在路由切换的过程中,对特定的路由进行访问控制。通过路由守卫,我们可以实现诸如验证用户登录状态、检查用户权限等功能,从而确保应用的安全性和用户体验。

React 中常见的路由库如 react - router - dom 提供了实现路由守卫的机制。路由守卫可以分为全局守卫、路由独享守卫和组件内守卫。

全局守卫

全局守卫应用于整个应用的路由系统。在 react - router - dom 中,可以通过 history 对象和 Location 对象来实现全局守卫。例如,我们可以在应用的入口处设置一个全局守卫,检查用户是否登录,如果未登录则重定向到登录页面。

import React from'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from'react - router - dom';
import Login from './components/Login';
import Dashboard from './components/Dashboard';
import { useSelector } from'react - redux';

const PrivateRoute = ({ children }) => {
    const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
    return isLoggedIn? children : <Navigate to="/login" />;
};

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/dashboard" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
            </Routes>
        </Router>
    );
};

export default App;

在上述代码中,PrivateRoute 组件起到了全局守卫的作用。它通过 useSelector 从 Redux 状态中获取用户的登录状态 isLoggedIn。如果用户已登录,则渲染子组件(即 Dashboard 组件);否则,将用户重定向到 /login 页面。

路由独享守卫

路由独享守卫只应用于特定的路由。在 react - router - dom 中,可以通过在路由配置中添加自定义的守卫逻辑来实现。例如,我们有一个管理页面,只有管理员用户才能访问。

import React from'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from'react - router - dom';
import Login from './components/Login';
import AdminDashboard from './components/AdminDashboard';
import { useSelector } from'react - redux';

const AdminRoute = ({ children }) => {
    const userRole = useSelector(state => state.auth.role);
    return userRole === 'admin'? children : <Navigate to="/login" />;
};

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/admin - dashboard" element={<AdminRoute><AdminDashboard /></AdminRoute>} />
            </Routes>
        </Router>
    );
};

export default App;

在这段代码中,AdminRoute 组件作为路由独享守卫。它通过 useSelector 获取用户角色 userRole,只有当用户角色为 admin 时,才允许访问 AdminDashboard 组件,否则重定向到 /login 页面。

组件内守卫

组件内守卫是在组件内部定义的守卫逻辑。它可以在组件即将挂载或卸载时执行特定的逻辑。例如,在一个需要用户权限验证的组件中,可以在组件的 componentDidMount 生命周期方法中检查用户权限。

import React, { Component } from'react';
import { withRouter } from'react - router - dom';
import { useSelector } from'react - redux';

class RestrictedComponent extends Component {
    componentDidMount() {
        const userRole = useSelector(state => state.auth.role);
        if (userRole!=='admin') {
            this.props.history.push('/login');
        }
    }

    render() {
        return (
            <div>
                <h1>Restricted Content</h1>
            </div>
        );
    }
}

export default withRouter(RestrictedComponent);

在上述代码中,RestrictedComponent 组件通过 componentDidMount 方法在组件挂载时检查用户角色。如果用户不是管理员,使用 this.props.history.push 将用户重定向到 /login 页面。这里使用了 withRouter 高阶组件来获取 history 对象,以便进行路由导航。

权限控制的基本原理

权限控制是基于用户的角色和权限来决定用户对应用资源的访问能力。在 React 应用中,权限控制通常与路由守卫结合使用。

用户角色与权限的定义

首先,我们需要定义不同的用户角色以及每个角色所拥有的权限。例如,我们可以定义三种角色:普通用户(user)、管理员(admin)和访客(guest)。普通用户可能只能查看某些页面,管理员可以进行所有操作,而访客可能只能访问有限的公共页面。

在代码中,可以通过一个对象来定义角色和权限:

const roles = {
    user: ['view - profile', 'create - post'],
    admin: ['view - profile', 'create - post', 'delete - post', 'edit - user'],
    guest: ['view - public - page']
};

权限验证逻辑

权限验证逻辑根据用户当前的角色,检查其是否具有访问特定资源或执行特定操作的权限。在 React 中,我们可以在路由守卫或组件内部实现这种验证逻辑。

例如,在一个需要特定权限才能访问的页面组件中:

import React, { Component } from'react';
import { withRouter } from'react - router - dom';
import { useSelector } from'react - redux';

class PrivilegedPage extends Component {
    componentDidMount() {
        const userRole = useSelector(state => state.auth.role);
        const requiredPermission = 'delete - post';
        const userPermissions = roles[userRole];
        if (!userPermissions.includes(requiredPermission)) {
            this.props.history.push('/forbidden');
        }
    }

    render() {
        return (
            <div>
                <h1>Privileged Page</h1>
            </div>
        );
    }
}

export default withRouter(PrivilegedPage);

在上述代码中,PrivilegedPage 组件在挂载时检查用户角色对应的权限是否包含 delete - post 权限。如果不包含,则将用户重定向到 /forbidden 页面。

基于角色的权限控制实现

基于角色的权限控制是一种常见的权限控制方式,它根据用户所属的角色来授予相应的权限。

配置角色相关路由

在路由配置中,我们可以根据角色来配置可访问的路由。例如,管理员角色可以访问所有管理相关的路由,而普通用户只能访问部分路由。

import React from'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from'react - router - dom';
import Login from './components/Login';
import UserDashboard from './components/UserDashboard';
import AdminDashboard from './components/AdminDashboard';
import { useSelector } from'react - redux';

const UserRoute = ({ children }) => {
    const userRole = useSelector(state => state.auth.role);
    return userRole === 'user'? children : <Navigate to="/login" />;
};

const AdminRoute = ({ children }) => {
    const userRole = useSelector(state => state.auth.role);
    return userRole === 'admin'? children : <Navigate to="/login" />;
};

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/user - dashboard" element={<UserRoute><UserDashboard /></UserRoute>} />
                <Route path="/admin - dashboard" element={<AdminRoute><AdminDashboard /></AdminRoute>} />
            </Routes>
        </Router>
    );
};

export default App;

在上述代码中,UserRouteAdminRoute 分别作为普通用户和管理员用户的路由守卫。根据用户角色来决定是否允许访问对应的 UserDashboardAdminDashboard 组件。

组件内基于角色的权限展示

除了路由层面的权限控制,在组件内部也可能需要根据用户角色来展示不同的内容。例如,在一个文章管理页面,管理员可以看到删除文章的按钮,而普通用户则看不到。

import React from'react';
import { useSelector } from'react - redux';

const ArticleManagement = () => {
    const userRole = useSelector(state => state.auth.role);
    return (
        <div>
            <h1>Article Management</h1>
            {userRole === 'admin' && <button>Delete Article</button>}
        </div>
    );
};

export default ArticleManagement;

在上述代码中,通过 userRole === 'admin' 的条件判断,只有管理员角色的用户才能看到 Delete Article 按钮。

基于资源的权限控制实现

基于资源的权限控制关注用户对特定资源的访问权限。例如,用户可能对自己创建的文章有编辑权限,而对其他用户的文章只有查看权限。

资源标识与权限映射

首先,我们需要为每个资源定义唯一的标识,并建立资源标识与权限的映射关系。例如,文章资源可以通过文章的 id 作为标识,权限可以包括 vieweditdelete 等。

const articlePermissions = {
    'article - 1': {
        'user - 1': ['view', 'edit', 'delete'],
        'user - 2': ['view']
    },
    'article - 2': {
        'user - 2': ['view', 'edit', 'delete'],
        'user - 1': ['view']
    }
};

在上述代码中,articlePermissions 对象定义了不同文章(通过 article - id 标识)对于不同用户(通过 user - id 标识)的权限。

组件内基于资源的权限控制

在涉及资源操作的组件中,我们可以根据当前用户和资源的权限映射来控制操作。例如,在文章详情页面:

import React from'react';
import { useSelector } from'react - redux';

const ArticleDetail = ({ articleId }) => {
    const userId = useSelector(state => state.auth.userId);
    const permissions = articlePermissions[articleId][userId];
    return (
        <div>
            <h1>Article Detail</h1>
            {permissions.includes('edit') && <button>Edit Article</button>}
            {permissions.includes('delete') && <button>Delete Article</button>}
        </div>
    );
};

export default ArticleDetail;

在上述代码中,ArticleDetail 组件根据当前用户的 userId 和文章的 articleIdarticlePermissions 中获取用户对该文章的权限。然后根据权限来决定是否展示 Edit ArticleDelete Article 按钮。

动态权限控制实现

动态权限控制允许在应用运行过程中根据用户的操作或系统状态动态地调整用户的权限。

权限更新机制

权限更新机制可以通过后端接口或前端状态管理来实现。例如,当用户完成某个任务或达到某个条件时,后端服务器可以发送更新权限的消息给前端。

在前端,我们可以使用 Redux 来管理权限状态。例如,当接收到权限更新的消息时,通过 Redux action 和 reducer 来更新权限状态。

// actions.js
const UPDATE_PERMISSIONS = 'UPDATE_PERMISSIONS';

export const updatePermissions = (permissions) => ({
    type: UPDATE_PERMISSIONS,
    payload: permissions
});

// reducer.js
const initialState = {
    permissions: []
};

const authReducer = (state = initialState, action) => {
    switch (action.type) {
        case UPDATE_PERMISSIONS:
            return {
               ...state,
                permissions: action.payload
            };
        default:
            return state;
    }
};

export default authReducer;

在上述代码中,updatePermissions action 用于更新权限,authReducer 通过 UPDATE_PERMISSIONS 类型的 action 来更新权限状态。

动态路由守卫与权限检查

在动态权限控制下,路由守卫和组件内的权限检查也需要动态更新。例如,在路由守卫中,根据最新的权限状态来决定是否允许访问路由。

import React from'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from'react - router - dom';
import Login from './components/Login';
import PrivilegedPage from './components/PrivilegedPage';
import { useSelector } from'react - redux';

const PrivilegedRoute = ({ children }) => {
    const permissions = useSelector(state => state.auth.permissions);
    const requiredPermission = 'access - privileged - page';
    return permissions.includes(requiredPermission)? children : <Navigate to="/login" />;
};

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/privileged - page" element={<PrivilegedRoute><PrivilegedPage /></PrivilegedRoute>} />
            </Routes>
        </Router>
    );
};

export default App;

在上述代码中,PrivilegedRoute 路由守卫根据最新的权限状态来决定是否允许访问 PrivilegedPage 组件。如果用户的权限列表中包含 access - privileged - page 权限,则允许访问,否则重定向到 /login 页面。

实战案例:完整的 React 应用权限控制

接下来,我们通过一个完整的实战案例来展示 React 路由守卫与权限控制的综合应用。

应用场景描述

假设我们正在开发一个博客应用,该应用有普通用户和管理员两种角色。普通用户可以创建、查看和编辑自己的文章,管理员可以管理所有用户的文章,包括删除文章。

项目结构

src/
├── components/
│   ├── ArticleList.js
│   ├── ArticleCreate.js
│   ├── ArticleEdit.js
│   ├── Login.js
│   ├── Navbar.js
│   └── Sidebar.js
├── redux/
│   ├── actions/
│       ├── authActions.js
│       └── articleActions.js
│   ├── reducers/
│       ├── authReducer.js
│       └── articleReducer.js
│   └── store.js
├── routes/
│   ├── PrivateRoutes.js
│   └── PublicRoutes.js
├── App.js
└── index.js

路由配置与守卫

routes/PrivateRoutes.js 中,我们定义了需要用户登录才能访问的路由:

import React from'react';
import { Routes, Route, Navigate } from'react - router - dom';
import { useSelector } from'react - redux';
import ArticleList from '../components/ArticleList';
import ArticleCreate from '../components/ArticleCreate';
import ArticleEdit from '../components/ArticleEdit';

const PrivateRoutes = () => {
    const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
    return (
        isLoggedIn? (
            <Routes>
                <Route path="/articles" element={<ArticleList />} />
                <Route path="/articles/create" element={<ArticleCreate />} />
                <Route path="/articles/edit/:id" element={<ArticleEdit />} />
            </Routes>
        ) : (
            <Navigate to="/login" />
        )
    );
};

export default PrivateRoutes;

routes/PublicRoutes.js 中,定义了公共路由:

import React from'react';
import { Routes, Route } from'react - router - dom';
import Login from '../components/Login';

const PublicRoutes = () => {
    return (
        <Routes>
            <Route path="/login" element={<Login />} />
        </Routes>
    );
};

export default PublicRoutes;

App.js 中,整合公共路由和私有路由:

import React from'react';
import { BrowserRouter as Router } from'react - router - dom';
import PublicRoutes from './routes/PublicRoutes';
import PrivateRoutes from './routes/PrivateRoutes';

const App = () => {
    return (
        <Router>
            <PublicRoutes />
            <PrivateRoutes />
        </Router>
    );
};

export default App;

权限控制在组件中的应用

ArticleList.js 中,根据用户角色显示不同的操作按钮:

import React from'react';
import { useSelector } from'react - redux';

const ArticleList = () => {
    const userRole = useSelector(state => state.auth.role);
    return (
        <div>
            <h1>Article List</h1>
            {userRole === 'admin' && <button>Delete All Articles</button>}
        </div>
    );
};

export default ArticleList;

ArticleEdit.js 中,根据用户权限判断是否允许编辑文章:

import React from'react';
import { useSelector } from'react - redux';

const ArticleEdit = ({ match }) => {
    const userId = useSelector(state => state.auth.userId);
    const article = useSelector(state => state.articles.find(article => article.id === match.params.id));
    const canEdit = article.authorId === userId || useSelector(state => state.auth.role) === 'admin';
    return (
        <div>
            <h1>Edit Article</h1>
            {canEdit && <input type="text" />}
        </div>
    );
};

export default ArticleEdit;

通过上述实战案例,我们全面展示了 React 路由守卫与权限控制在实际应用中的实现方式,从路由配置到组件内的权限判断,构建了一个完整的权限控制体系。

安全考虑与优化

在实现 React 路由守卫与权限控制时,有几个重要的安全考虑和优化点需要注意。

防止未授权访问

尽管我们通过路由守卫和权限控制在前端进行了访问限制,但前端的限制是可以被绕过的。因此,后端也需要进行严格的权限验证。在后端 API 接口中,每次请求都应该检查用户的权限,确保只有授权用户才能执行相应的操作。

例如,在 Node.js 和 Express 构建的后端服务中:

const express = require('express');
const app = express();
const jwt = require('jsonwebtoken');

const verifyToken = (req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(401).send('Access denied. No token provided.');
    }
    try {
        const decoded = jwt.verify(token, 'your - secret - key');
        req.user = decoded;
        next();
    } catch (error) {
        res.status(400).send('Invalid token.');
    }
};

app.get('/admin - api', verifyToken, (req, res) => {
    if (req.user.role!== 'admin') {
        return res.status(403).send('Forbidden. Only admins can access this.');
    }
    res.send('Admin API response');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,verifyToken 中间件用于验证 JWT 令牌,确保请求来自授权用户。对于 /admin - api 接口,进一步检查用户角色是否为 admin,如果不是则返回 403 Forbidden 错误。

优化性能

过多的路由守卫和复杂的权限检查逻辑可能会影响应用的性能。为了优化性能,可以采取以下措施:

  • 缓存权限数据:在前端,将用户的权限数据缓存起来,避免每次路由切换或组件渲染时都进行权限查询。例如,可以使用 localStorage 或 Redux 来缓存权限数据。
  • 懒加载路由组件:对于一些不常用或权限要求较高的路由组件,可以采用懒加载的方式。这样在应用启动时,不会一次性加载所有组件,从而提高应用的加载速度。
import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';

const Home = React.lazy(() => import('./components/Home'));
const AdminDashboard = React.lazy(() => import('./components/AdminDashboard'));

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/" element={
                    <React.Suspense fallback={<div>Loading...</div>}>
                        <Home />
                    </React.Suspense>
                } />
                <Route path="/admin - dashboard" element={
                    <React.Suspense fallback={<div>Loading...</div>}>
                        <AdminDashboard />
                    </React.Suspense>
                } />
            </Routes>
        </Router>
    );
};

export default App;

在上述代码中,HomeAdminDashboard 组件采用懒加载的方式,只有在访问相应路由时才会加载组件代码。

错误处理

在路由守卫和权限控制过程中,可能会出现各种错误,如权限验证失败、路由配置错误等。良好的错误处理机制可以提高应用的稳定性和用户体验。

在路由守卫中,可以统一处理权限验证失败的情况,例如重定向到一个友好的错误页面。

import React from'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from'react - router - dom';
import Login from './components/Login';
import Forbidden from './components/Forbidden';
import { useSelector } from'react - redux';

const PrivateRoute = ({ children }) => {
    const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
    if (!isLoggedIn) {
        return <Navigate to="/login" />;
    }
    const hasPermission = useSelector(state => state.auth.permissions.includes('required - permission'));
    if (!hasPermission) {
        return <Navigate to="/forbidden" />;
    }
    return children;
};

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/forbidden" element={<Forbidden />} />
                <Route path="/protected - page" element={<PrivateRoute><div>Protected Page</div></PrivateRoute>} />
            </Routes>
        </Router>
    );
};

export default App;

在上述代码中,当用户没有权限访问 protected - page 时,会被重定向到 /forbidden 页面,该页面可以展示友好的错误提示信息。

通过以上安全考虑和优化措施,可以使 React 应用的路由守卫与权限控制更加健壮、高效和安全。