这是一个React Router教程,教你如何在React Router 6中使用验证功能。这个React Router v6教程的代码可以在这里找到。为了让你开始,创建一个新的React项目(例如:create-react-app)。之后,安装React Router,并阅读下面的React Router教程,让自己与接下来的内容保持一致。
我们将从一个最小的React项目开始,它使用React Router将用户从一个页面导航到另一个页面。在下面的功能组件中,我们有来自React Router的匹配的Link和Route组件,用于home/ 和dashboard/ 路径。此外,我们还有一个用Home组件加载的所谓Index Route和一个用NoMatch组件加载的所谓No Match Route。两者都作为后备路由。
import { Routes, Route, Link } from 'react-router-dom';
const App = () => {
return (
<>
<h1>React Router</h1>
<Navigation />
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};
const Navigation = () => {
return (
<nav>
<NavLink to="/home">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
</nav>
);
};
从这里开始,我们将探索React Router的认证概念。一般来说,React Router并不处理认证本身,它关心的是与认证有关的导航。
因此,无论你是针对REST API、GraphQL API,还是Firebase这样的后台即服务进行认证,都取决于你。最终重要的是,认证API在认证成功后返回给你的前端一个令牌(例如JWT),React Router将从那里接管(例如在登录后重定向用户)。
我们将使用一个假的API来模拟对后端的认证。这个假的API只是一个函数,它从一个有延迟的承诺中解析一个字符串。然而,如果你有一个支持认证的后端,你可以使用后端API,而不需要在你的前端实现下面的函数。
const fakeAuth = () =>
new Promise((resolve) => {
setTimeout(() => resolve('2342f2f1d131rf12'), 250);
});
但让我们从简单的开始。在前面的例子中,我们为一个主页和一个仪表板组件创建了两条路由。这些组件可以通过以下方式实现,并且已经表明它们是否可以被授权用户访问。
const Home = () => {
return (
<>
<h2>Home (Public)</h2>
</>
);
};
const Dashboard = () => {
return (
<>
<h2>Dashboard (Protected)</h2>
</>
);
};
公共的主页组件应该是每个人都可以访问的,而受保护的仪表盘组件应该是只有经过认证的用户才可以访问的。目前,你可以导航到这两个组件,我们将在以后通过使用所谓的私有路线来实现对仪表板组件的保护。
现在,我们将通过实现一个带有回调处理程序的按钮来登录用户,首先关注用户的认证问题。我们在这里使用 "主页",但如果你想的话,你也可以使用一个专门的 "登录页面"。
const Home = ({ onLogin }) => {
return (
<>
<h2>Home (Public)</h2>
<button type="button" onClick={onLogin}>
Sign In
</button>
</>
);
};
在现实世界中,你会使用一堆HTML表单元素来捕捉用户的电子邮件/密码组合,并在用户提交表单时通过回调处理程序将其传递上去。然而,为了保持简单,我们在这里只使用一个按钮。
接下来,在父组件中,我们创建实际的事件处理程序,通过React道具传递给主页组件作为回调处理程序,每当用户点击主页组件中的按钮时,就会调用该处理程序。在回调处理程序中,我们执行假的API,为我们返回一个令牌。同样,如果你有自己的后台,有一个认证API,你可以用真正的后台来认证。
const App = () => {
const [token, setToken] = React.useState(null);
const handleLogin = async () => {
const token = await fakeAuth();
setToken(token);
};
return (
<>
<h1>React Router</h1>
<Navigation />
<Routes>
<Route index element={<Home onLogin={handleLogin} />} />
<Route path="home" element={<Home onLogin={handleLogin} />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};
此外,我们使用React的useState Hook将令牌存储为组件状态。令牌本身就是一个认证用户的代表。在现实世界的场景中,你可能有一个JWT令牌,它封装了用户的信息(如用户名、电子邮件)。
一个有登录的应用程序也需要有一个对应的注销。在我们的案例中,注销将在顶层的导航组件中启动,但可以随意将其放在你想要的任何地方。在传递给导航组件的新回调处理程序中,我们将只在用户从应用中注销时将组件状态中的令牌重置为null 。
const App = () => {
const [token, setToken] = React.useState(null);
const handleLogin = async () => {
const token = await fakeAuth();
setToken(token);
};
const handleLogout = () => {
setToken(null);
};
return (
<>
<h1>React Router</h1>
<Navigation token={token} onLogout={handleLogout} />
...
</>
);
};
如果你自己与真正的后端进行交互,有时你也必须调用API进行注销(例如,在后端使会话无效)。总之,有了新的回调处理程序,签出用户的时候,只要这个用户被认证了(比如token不是null ),我们就有条件地给用户显示一个注销按钮。
const Navigation = ({ token, onLogout }) => {
return (
<nav>
<NavLink to="/home">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
{token && (
<button type="button" onClick={onLogout}>
Sign Out
</button>
)}
</nav>
);
};
一旦你尝试了你的React应用程序,你会发现新的 "注销 "按钮只在你点击主页上的 "登录 "按钮时出现。如果你在登录后点击 "Sign Out "按钮,"Sign Out "按钮应该再次消失。
注意事项:在现实世界的React应用程序中,如果是未认证的用户,对仪表板页面的导航也会被隐藏。然而,为了在实现认证流程时调试这一切,我们将显示导航。
认证的背景
当在一个应用程序中验证用户时,你很可能希望得到关于用户是否在不同组件中被验证的信息。直接的方法是通过props将token 传递给所有对认证状态感兴趣的组件。然而,你最有可能看到的是使用React Context将props从顶层传到子组件,而不使用props。
const AuthContext = React.createContext(null);
const App = () => {
const [token, setToken] = React.useState(null);
...
return (
<AuthContext.Provider value={token}>
<h1>React Router</h1>
<Navigation onLogout={handleLogout} />
<Routes>
...
</Routes>
</AuthContext.Provider>
);
};
在我们在应用程序的顶层创建了上下文,并将值(这里是:token )传递给Context的提供者组件后,我们可以在应用程序的某个地方消费该上下文。例如,为了在Dashboard组件中显示认证状态,而不需要将令牌作为道具传递,我们可以使用React的useContext Hook,它从Provider组件中返回值。
const Dashboard = () => {
const token = React.useContext(AuthContext);
return (
<>
<h2>Dashboard (Protected)</h2>
<div>Authenticated as {token}</div>
</>
);
};
基本上这就是在React中使用裸露的上下文。然而,如果我们想在使用React的useContext Hook时遵循最佳实践,我们可以将上下文抽象为更多的自描述性的东西--此外,这也屏蔽了认证过程的所有内部实施细节。
const AuthProvider = ({ children }) => {
const [token, setToken] = React.useState(null);
const handleLogin = async () => {
const token = await fakeAuth();
setToken(token);
};
const handleLogout = () => {
setToken(null);
};
const value = {
token,
onLogin: handleLogin,
onLogout: handleLogout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
通过将所有的实现细节转移到一个自定义的Provider组件中,App组件不再被所有的认证相关的业务逻辑所困扰。相反,所有的逻辑都存在于新的提供者组件中。
const App = () => {
return (
<AuthProvider>
<h1>React Router</h1>
<Navigation />
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</AuthProvider>
);
};
由于上下文中的value 从字符串变成了具有token (状态)、onLogin (事件处理程序)和onLogout (事件处理程序)的对象,我们必须调整我们以前使用的消耗上下文钩子,其中token 需要从对象中解构。
const Dashboard = () => {
const { token } = React.useContext(AuthContext);
return (
<>
<h2>Dashboard (Protected)</h2>
<div>Authenticated as {token}</div>
</>
);
};
为了再次遵循useContext的最佳实践,我们可以用一个自我描述的名字创建一个自定义钩子。
const useAuth = () => {
return React.useContext(AuthContext);
};
然后,我们又可以用这个新的自定义React钩子来取代赤裸裸的useContext 的用法。在一个更大的React项目中,这种抽象可以帮助清理你的React代码。
const Dashboard = () => {
const { token } = useAuth();
return (
<>
<h2>Dashboard (Protected)</h2>
<div>Authenticated as {token}</div>
</>
);
};
之前在App组件中定义并向下传递给组件的事件处理程序,现在被定义在自定义的Provider组件中。因此,我们不再将这些事件处理程序作为回调处理程序从App组件中传递下来,而是将这些事件处理程序作为函数从新的上下文中通过重构它们来消费。
const Navigation = () => {
const { onLogout } = useAuth();
return (
<nav>
<NavLink to="/home">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
{token && (
<button type="button" onClick={onLogout}>
Sign Out
</button>
)}
</nav>
);
};
const Home = () => {
const { onLogin } = useAuth();
return (
<>
<h2>Home (Public)</h2>
<button type="button" onClick={onLogin}>
Sign In
</button>
</>
);
};
这就是在React中使用更复杂的上下文认证方法。我们已经创建了一个自定义的Provider组件,它可以跟踪token 状态(读作:认证状态)。此外,我们在新的自定义提供者组件中定义了所有必要的处理程序(例如,登录、注销),而不是用这些实现细节来扰乱App组件。然后我们把状态和事件处理程序作为上下文传递给所有对认证状态和/或签入/签出用户感兴趣的组件。
认证后的React Router重定向
我们已经有了所有基本认证的业务逻辑,并且能够在React的上下文(这里:自定义useAuth 钩子)的帮助下,在React应用程序的任何地方消费这个业务逻辑(状态+事件处理程序)。
接下来React Router终于发挥作用了,因为在成功认证后,用户通常会从登录页面(这里:主页)被重定向到一个登陆页面(这里:仪表盘页面),而后者只有通过认证的用户才能访问。
import {
Routes,
Route,
NavLink,
useNavigate,
} from 'react-router-dom';
...
const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const [token, setToken] = React.useState(null);
const handleLogin = async () => {
const token = await fakeAuth();
setToken(token);
navigate('/dashboard');
};
const handleLogout = () => {
setToken(null);
};
const value = {
token,
onLogin: handleLogin,
onLogout: handleLogout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
我们通过React Router的useNavigate Hook,以编程方式处理重定向。然而,明确的重定向只适用于签入。相反,对于签出,我们将使用隐式重定向,为敏感页面(阅读:组件)创建一个所谓的保护路由,禁止未认证的用户访问。
React Router中的受保护路由
让我们利用受保护的路由(也叫私有路由)。因此,我们将创建一个新的组件。在保护未经授权的用户(这里:未认证的用户)的情况下,该组件将检查认证令牌是否存在。如果它是存在的,该组件将呈现其子女。然而,如果它不存在,用户就会得到一个有条件的重定向,用React Router的声明性Navigate组件到登录页面(这里:主页)。
import {
Routes,
Route,
NavLink,
Navigate,
useNavigate,
} from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
if (!token) {
return <Navigate to="/home" replace />;
}
return children;
};
接下来我们将使用这个新组件。在我们的App组件中,Dashboard组件应该只被认证的用户访问。因此,新的ProtectedRoute组件被包裹在它周围。
const App = () => {
return (
<AuthProvider>
<h1>React Router</h1>
<Navigation />
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route
path="dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="*" element={<NoMatch />} />
</Routes>
</AuthProvider>
);
};
现在,当用户点击按钮注销时,他们会通过新的保护路由得到一个隐含的重定向,因为令牌已经不存在了。此外,如果一个用户没有经过认证,这个用户就不可能访问一个受保护的路由(这里:仪表盘页面)。
继续阅读。React Router 6 私人路由
请注意。无论如何,即使路由被保护,不再被未经授权的用户访问,一个恶意的用户仍然可以修改浏览器中的客户端代码(例如,删除从ProtectedRoute重定向的条件)。因此,所有发生在受保护页面(如仪表盘页面)的敏感API调用也需要从服务器端进行安全保护。
记住重定向的路线
在现代应用中,你会在登录后得到一个重定向到你之前访问过的页面。换句话说。如果你在一个受保护的路径上打开一个应用程序,但你没有登录,你会得到一个重定向到登录页面。登录后,你会得到一个重定向到所需的受保护路径。
为了实现这样的智能重定向,我们必须 "记住 "重定向发生在登录页面的位置。添加这些实现细节的最好地方是ProtectedRoute组件。在那里我们可以使用React Router的useLocation Hook,在重定向用户之前抓取当前的位置。随着重定向的进行,我们也将当前页面的状态发送到被重定向的页面。
import {
Routes,
Route,
NavLink,
Navigate,
useNavigate,
useLocation,
} from 'react-router-dom';
...
const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
const location = useLocation();
if (!token) {
return <Navigate to="/home" replace state={{ from: location }} />;
}
return children;
};
接下来我们可以再次从React Router的位置抓取与前一个页面的状态。当登录发生时,我们可以拿前一个页面来重定向用户到这个想要的页面。如果这个页面从未被设置为状态,我们就默认为仪表盘页面。
const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const [token, setToken] = React.useState(null);
const handleLogin = async () => {
const token = await fakeAuth();
setToken(token);
const origin = location.state?.from?.pathname || '/dashboard';
navigate(origin);
};
...
};
目前我们只有一个受保护的页面,所以很难测试新的智能重定向行为。然而,你可以快速添加第二个受保护的页面来自己测试。
const App = () => {
return (
<AuthProvider>
<h1>React Router</h1>
<Navigation />
<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route
path="dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="admin"
element={
<ProtectedRoute>
<Admin />
</ProtectedRoute>
}
/>
<Route path="*" element={<NoMatch />} />
</Routes>
</AuthProvider>
);
};
const Navigation = () => {
const { token, onLogout } = useAuth();
return (
<nav>
<NavLink to="/home">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
<NavLink to="/admin">Admin</NavLink>
{token && (
<button type="button" onClick={onLogout}>
Sign Out
</button>
)}
</nav>
);
};
const Admin = () => {
return (
<>
<h2>Admin (Protected)</h2>
</>
);
};
当你以未认证的用户身份访问管理页面时,你会得到一个重定向到主页。在成功登录后,你会得到一个重定向回到管理页面。当你以非认证用户身份导航到仪表板时,也会发生同样的情况。登录后,你将得到一个重定向到记忆中的仪表板页面。
这就是了。你已经用React Router和一个假的API创建了一个认证流程。你可以随时用你的实际后端API来交换这个假的API。此外,你可以有条件地隐藏链接组件,在用户没有被认证的情况下将用户导航到受保护的路由。你也可以创建一个专门的登录页面,让用户得到一个表单,要求输入电子邮件/用户+密码的组合。