如何在 React 应用程序中添加登录验证

作者选择知识共享接受捐赠,作为WRITE FOR DIRECTIONS计划)的一部分。

简介

许多Web应用程序是公共和私有页面的混合。公共页面对任何人都可用,而私人页面需要用户登录。您可以使用_authentication_来管理哪些用户可以访问哪些页面。您的React应用程序将需要处理用户在登录之前尝试访问私有页面的情况,并且您需要在他们成功认证后保存登录信息。

在本教程中,您将使用基于令牌的身份验证系统创建一个Reaction应用程序。您将创建一个返回用户令牌的模拟API,构建一个获取令牌的登录页面,并在不重新路由用户的情况下检查身份验证。如果用户未通过身份验证,您将为他们提供登录的机会,然后允许他们在不导航到专用登录页面的情况下继续。在构建应用程序时,您将探索存储令牌的不同方法,并了解每种方法的安全性和体验权衡。本教程将重点介绍在LocalStoragesessionStorage.中存储令牌

在本教程结束时,您将能够向Reaction应用程序添加身份验证,并将登录和令牌存储策略集成到一个完整的用户工作流中。

<$>[信息] 需要快速部署Reaction项目吗?查看DigitalOcean App Platform并在几分钟内直接从GitHub部署Reaction项目。 <$>

前提条件

第一步-构建登录页面

在本步骤中,您将为应用程序创建一个登录页。您将首先安装Reaction Router并创建组件来表示完整的应用程序。然后,您将在任何路径上呈现登录页面,以便您的用户可以登录到应用程序,而不会被重定向到新页面。

在本步骤结束时,您将拥有一个基本的应用程序,该应用程序将在用户未登录到应用程序时呈现登录页面。

首先,安装带有npm的REACT路由器。有两个不同的版本:Web版本和用于Reaction Native.)的本机版本安装Web版本:

1npm install react-router-dom

将安装该程序包,安装完成后您将收到一条消息。您的消息可能会略有不同:

1[secondary_label Output]
2...
3+ [email protected]
4added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
5...

接下来,创建两个名为DashboardPferencescomponents作为私有页面。这些组件将代表用户在成功登录到应用程序之前不应看到的组件。

首先,创建目录:

1mkdir src/components/Dashboard
2mkdir src/components/Preferences

然后在文本编辑器中打开Dashboard.js。本教程将使用Nano

1nano src/components/Dashboard/Dashboard.js

Dashboard.js内,添加一个<h2>标签,内容为Dashboard

1[label auth-tutorial/src/components/Dashboard/Dashboard.js]
2import React from 'react';
3
4export default function Dashboard() {
5  return(
6    <h2>Dashboard</h2>
7  );
8}

保存并关闭该文件。

首选项重复相同的步骤。打开组件:

1nano src/components/Preferences/Preferences.js

添加内容:

1[label auth-tutorial/src/components/Preferences/Preferences.js]
2import React from 'react';
3
4export default function Preferences() {
5  return(
6    <h2>Preferences</h2>
7  );
8}

保存并关闭该文件。

现在您已经有了一些组件,您需要导入组件并在App.js中创建路由。查看教程How to Handle Routing in Reaction Apps With Reaction Router》,了解有关React应用程序中的路由的完整介绍。

首先,打开App.js

1nano src/components/App/App.js

然后通过添加以下突出显示的代码导入DashboardPferences

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import './App.css';
 4import Dashboard from '../Dashboard/Dashboard';
 5import Preferences from '../Preferences/Preferences';
 6
 7function App() {
 8  return (
 9    <></>
10  );
11}
12
13export default App;

接下来,从react-router-dom导入BrowserRouterSwitchRoute

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import './App.css';
 4import { BrowserRouter, Route, Switch } from 'react-router-dom';
 5import Dashboard from '../Dashboard/Dashboard';
 6import Preferences from '../Preferences/Preferences';
 7
 8function App() {
 9  return (
10    <></>
11  );
12}
13
14export default App;

添加一个classNamewrapper<h1>标签的<div>,作为应用程序的模板。请确保您正在导入App.css,以便可以应用样式。

接下来,为DashboardPferences组件创建路由。添加BrowserRouter,然后添加一个Switch组件作为子组件。在Switch内,添加一个Route,每个组件都有一个路径

 1[label tutorial/src/components/App/App.js]
 2import React from 'react';
 3import './App.css';
 4import { BrowserRouter, Route, Switch } from 'react-router-dom';
 5import Dashboard from '../Dashboard/Dashboard';
 6import Preferences from '../Preferences/Preferences';
 7
 8function App() {
 9  return (
10    <div className="wrapper">
11      <h1>Application</h1>
12      <BrowserRouter>
13        <Switch>
14          <Route path="/dashboard">
15            <Dashboard />
16          </Route>
17          <Route path="/preferences">
18            <Preferences />
19          </Route>
20        </Switch>
21      </BrowserRouter>
22    </div>
23  );
24}
25
26export default App;

保存并关闭该文件。

最后一步是给main 添加一些填充

,这样你的组件就不会直接位于浏览器的边缘。为此,您需要更改CSS

打开App.css

1nano src/components/App/App.css

将内容替换为20pxpadding的类.wrapper

1[label auth-tutorial/src/components/App/App.css]
2.wrapper {
3    padding: 20px;
4}

保存并关闭该文件。当您执行此操作时,浏览器将重新加载,您将找到基本组件:

基础组件

检查每条路线。如果您访问http://localhost:3000/dashboard,,您将看到仪表板页面:

仪表板Component

您的路由工作正常,但有一个小问题。路由/dashboard应该是受保护的页面,并且不应该被未经身份验证的用户查看。有不同的方法来处理个人主页。例如,您可以为登录页面创建一条新的路径,并使用Reaction Router to Redicate if the User is Logging in.这是一个很好的方法,但用户会失去路线,不得不导航回他们最初想要查看的页面。

一种侵入性较小的选项是生成登录页面,而不考虑路线。使用这种方法,如果没有存储的用户令牌,您将呈现一个登录页面,并且当用户登录时,他们将位于他们最初访问的同一路线上。也就是说,如果用户访问/dashboard,则登录后仍在/dashboard路径上。

首先,为Login组件创建一个新目录:

1mkdir src/components/Login

接下来,在文本编辑器中打开Login.js

1nano src/components/Login/Login.js

创建一个基本表单,其中提交<按钮>,用户名和密码<输入>。请务必将密码输入类型设置为password

 1[label auth-tutorial/src/components/Login/Login.js]
 2import React from 'react';
 3
 4export default function Login() {
 5  return(
 6    <form>
 7      <label>
 8        <p>Username</p>
 9        <input type="text" />
10      </label>
11      <label>
12        <p>Password</p>
13        <input type="password" />
14      </label>
15      <div>
16        <button type="submit">Submit</button>
17      </div>
18    </form>
19  )
20}

有关Reaction中表单的更多信息,请查看教程如何在React.中构建表单

接下来,添加一个<h1>标签,要求用户登录。将<form><h1>包装在一个<div>中,classNamelogin-wrapper。最后,导入Login.css

 1[label auth-tutorial/src/components/Login/Login.js]
 2import React from 'react';
 3import './Login.css';
 4
 5export default function Login() {
 6  return(
 7    <div className="login-wrapper">
 8      <h1>Please Log In</h1>
 9      <form>
10        <label>
11          <p>Username</p>
12          <input type="text" />
13        </label>
14        <label>
15          <p>Password</p>
16          <input type="password" />
17        </label>
18        <div>
19          <button type="submit">Submit</button>
20        </div>
21      </form>
22    </div>
23  )
24}

保存并关闭该文件。

现在您已经有了一个基本的Login‘组件,您需要添加一些样式。打开Login.css`:

1nano src/components/Login/Login.css

将组件在页面居中,方法是添加flexdisplay,然后将flex-Direction设置为Column以垂直对齐元素,并在Center中添加Align-items以使组件在浏览器中居中:

1[label auth-tutorial/src/components/Login/Login.css]
2.login-wrapper {
3    display: flex;
4    flex-direction: column;
5    align-items: center;
6}

有关使用FlexBox的更多信息,请参阅我们的Css Flexbox Cheatsheet

保存并关闭该文件。

最后,如果没有用户令牌,则需要在App.js中呈现它。打开App.js

1nano src/components/App/App.js

步骤3 中,您将探索存储令牌的选项。目前,您可以使用useStateHook.将令牌存储在内存中

react导入useState,然后调用useState,并将返回值设置为tokensetToken

 1[label auth-tutorial/src/components/App/App.js]
 2import React, { useState } from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4import './App.css';
 5import Dashboard from '../Dashboard/Dashboard';
 6import Preferences from '../Preferences/Preferences';
 7
 8function App() {
 9  const [token, setToken] = useState();
10  return (
11    <div className="wrapper">
12      <h1>Application</h1>
13      <BrowserRouter>
14        <Switch>
15          <Route path="/dashboard">
16            <Dashboard />
17          </Route>
18          <Route path="/preferences">
19            <Preferences />
20          </Route>
21        </Switch>
22      </BrowserRouter>
23    </div>
24  );
25}
26
27export default App;

导入Login组件。如果statement](https://andsky.com/tech/tutorials/how-to-write-conditional-statements-in-javascript)为假,则添加[条件token以显示Login

setToken函数传递给Login组件:

 1[label auth-tutorial/src/components/App/App.js]
 2import React, { useState } from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4
 5import './App.css';
 6import Dashboard from '../Dashboard/Dashboard';
 7import Login from '../Login/Login';
 8import Preferences from '../Preferences/Preferences';
 9
10function App() {
11  const [token, setToken] = useState();
12
13  if(!token) {
14    return <Login setToken={setToken} />
15  }
16
17  return (
18    <div className="wrapper">
19      <h1>Application</h1>
20      <BrowserRouter>
21        <Switch>
22          <Route path="/dashboard">
23            <Dashboard />
24          </Route>
25          <Route path="/preferences">
26            <Preferences />
27          </Route>
28        </Switch>
29      </BrowserRouter>
30    </div>
31  );
32}
33
34export default App;

目前,没有令牌;在下一步中,您将调用API并使用返回值设置令牌。

保存并关闭该文件。当您这样做时,浏览器将重新加载,您将看到登录页面。请注意,如果您访问http://localhost:3000/dashboard,,您仍然会看到登录页面,因为令牌尚未设置:

登录页面

在本步骤中,您创建了一个具有私有组件和登录组件的应用程序,该组件将一直显示到您设置令牌为止。您还配置了显示页面的路由,并添加了复选框以在用户尚未登录应用程序的情况下在每条路由上显示Login组件。

在下一步中,您将创建一个返回用户令牌的本地API。您将从Login组件调用接口,并在成功时将令牌保存到内存。

Step 2 -创建Token接口

在本步骤中,您将创建一个本地API来获取用户令牌。您将使用Node.js构建一个mock API,它将返回一个用户令牌。然后,您将从登录页面调用该API,并在成功检索令牌后呈现组件。在本步骤结束时,您将拥有一个应用程序,其中包含一个有效的登录页面和受保护的页面,只有在登录后才能访问这些页面。

您将需要一个服务器来充当返回令牌的后端。您可以使用Node.js和Express Web框架.)快速创建服务器有关创建Express服务器的详细介绍,请参阅教程Node.js.中的基本Express服务器

要开始,请安装exples。由于服务器不是最终构建的要求,因此请确保作为devDependency.安装

您还需要安装cors.该库将为所有路线启用跨源资源sharing

<$>[警告] 警告: 不要对生产应用中的所有路由启用CORS。这可能会导致安全漏洞。 <$>

1npm install --save-dev express cors

安装完成后,您将收到一条成功消息:

1[secondary_label Output]
2...
3+ [email protected]
4+ [email protected]
5removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
6...

接下来,在应用程序的根目录中打开一个名为server.js的新文件。不要将此文件添加到/src目录,因为您不希望它成为最终构建的一部分。

1nano server.js

导入express,然后调用express()并将结果保存到一个名为app的变量中,从而初始化一个新的APP:

1[label auth-tutorial/server.js]
2const express = require('express');
3const app = express();

创建app后,添加cors作为middleware.首先导入cors,然后调用app上的use方法将其添加到应用程序中:

1[label auth-tutorial/server.js]
2const express = require('express');
3const cors = require('cors');
4const app = express();
5
6app.use(cors());

接下来,使用app.use收听特定的路由。第一个参数是应用程序将侦听的路径,第二个参数是回调function,它将在应用程序提供该路径时运行。回调使用req参数,该参数包含请求数据和处理结果的res参数。

/login路径添加一个句柄。使用包含token的JavaScript object调用res.send

 1[label auth-tutorial/server.js]
 2const express = require('express');
 3const cors = require('cors')
 4const app = express();
 5
 6app.use(cors());
 7
 8app.use('/login', (req, res) => {
 9  res.send({
10    token: 'test123'
11  });
12});

最后,使用app.listen在端口8080上运行服务器:

 1[label auth-tutorial/server.js]
 2const express = require('express');
 3const cors = require('cors')
 4const app = express();
 5
 6app.use(cors());
 7
 8app.use('/login', (req, res) => {
 9  res.send({
10    token: 'test123'
11  });
12});
13
14app.listen(8080, () => console.log('API is running on http://localhost:8080/login'));

保存并关闭该文件。在新的终端窗口或选项卡中,启动服务器:

1node server.js

您将收到指示服务器正在启动的响应:

1[secondary_label Output]
2API is running on http://localhost:8080/login

访问http://localhost:8080/login,您将找到您的Json object.

Token response

当您在浏览器中获取令牌时,您正在发出一个GET请求,但当您提交登录表单时,您将发出一个POST请求。这不是问题。当您使用app.use设置您的路由时,Express将统一处理所有请求。在生产应用中,您应该更具体,只允许对每条路由使用某些请求methods]。

现在您已经有了一个正在运行的API服务器,您需要从登录页面发出请求。打开Login.js

1nano src/components/Login/Login.js

在上一步中,您将一个名为setToken的新prop传递给了Login组件。添加新道具中的PropType,并对道具对象执行destructure操作,以拉出setToken道具。

 1[label auth-tutorial/src/components/Login/Login.js]
 2
 3import React from 'react';
 4import PropTypes from 'prop-types';
 5
 6import './Login.css';
 7
 8export default function Login({ setToken }) {
 9  return(
10    <div className="login-wrapper">
11      <h1>Please Log In</h1>
12      <form>
13        <label>
14          <p>Username</p>
15          <input type="text" />
16        </label>
17        <label>
18          <p>Password</p>
19          <input type="password" />
20        </label>
21        <div>
22          <button type="submit">Submit</button>
23        </div>
24      </form>
25    </div>
26  )
27}
28
29Login.propTypes = {
30  setToken: PropTypes.func.isRequired
31}

接下来,创建一个本地状态来捕获用户名密码。由于您不需要手动设置数据,因此请将<ins>设置为非受控组件。有关非受控组件的详细信息,请参阅How to Build Forms in React.

 1[label auth-tutorial/src/components/Login/Login.js]
 2import React, { useState } from 'react';
 3import PropTypes from 'prop-types';
 4
 5import './Login.css';
 6
 7export default function Login({ setToken }) {
 8  const [username, setUserName] = useState();
 9  const [password, setPassword] = useState();
10  return(
11    <div className="login-wrapper">
12      <h1>Please Log In</h1>
13      <form>
14        <label>
15          <p>Username</p>
16          <input type="text" onChange={e => setUserName(e.target.value)}/>
17        </label>
18        <label>
19          <p>Password</p>
20          <input type="password" onChange={e => setPassword(e.target.value)}/>
21        </label>
22        <div>
23          <button type="submit">Submit</button>
24        </div>
25      </form>
26    </div>
27  )
28}
29
30Login.propTypes = {
31  setToken: PropTypes.func.isRequired
32};

接下来,创建一个函数向服务器发出POST请求。在大型应用程序中,您需要将它们添加到单独的目录中。在本例中,您将直接将服务添加到组件。查看教程How to Call WebAPI with the useEffect Hook in React》,详细了解如何在Reaction组件中调用API。

创建名为loginUserasyncfunction。该函数将以redentials为参数,然后使用POST选项调用fetch方法:

 1[label auth-tutorial/src/components/Login/Login.js]
 2import React, { useState } from 'react';
 3import PropTypes from 'prop-types';
 4import './Login.css';
 5
 6async function loginUser(credentials) {
 7 return fetch('http://localhost:8080/login', {
 8   method: 'POST',
 9   headers: {
10     'Content-Type': 'application/json'
11   },
12   body: JSON.stringify(credentials)
13 })
14   .then(data => data.json())
15}
16
17export default function Login({ setToken }) {
18...

最后,创建一个名为handleSubmit的表单提交处理程序,它将使用usernamepassword调用loginUser。调用setToken,结果成功。在<form>上使用onSubmit事件处理函数调用handleSubmit

 1[label auth-tutorial/src/components/Login/Login.js]
 2import React, { useState } from 'react';
 3import PropTypes from 'prop-types';
 4import './Login.css';
 5
 6async function loginUser(credentials) {
 7 return fetch('http://localhost:8080/login', {
 8   method: 'POST',
 9   headers: {
10     'Content-Type': 'application/json'
11   },
12   body: JSON.stringify(credentials)
13 })
14   .then(data => data.json())
15}
16
17export default function Login({ setToken }) {
18  const [username, setUserName] = useState();
19  const [password, setPassword] = useState();
20
21  const handleSubmit = async e => {
22    e.preventDefault();
23    const token = await loginUser({
24      username,
25      password
26    });
27    setToken(token);
28  }
29
30  return(
31    <div className="login-wrapper">
32      <h1>Please Log In</h1>
33      <form onSubmit={handleSubmit}>
34        <label>
35          <p>Username</p>
36          <input type="text" onChange={e => setUserName(e.target.value)} />
37        </label>
38        <label>
39          <p>Password</p>
40          <input type="password" onChange={e => setPassword(e.target.value)} />
41        </label>
42        <div>
43          <button type="submit">Submit</button>
44        </div>
45      </form>
46    </div>
47  )
48}
49
50Login.propTypes = {
51  setToken: PropTypes.func.isRequired
52};

<$>[备注] 注意: 在完整的应用程序中,您需要处理组件在Promise解析之前卸载的情况。有关更多信息,请查看教程如何在React中使用useEffect钩子调用WebAPI。 <$>

保存并关闭该文件。确保您的本地应用编程接口仍在运行,然后打开浏览器访问http://localhost:3000/dashboard.

您将看到登录页面而不是仪表板。填写并提交表单,您将收到一个Web令牌,然后重定向到仪表板页面。

登录页面

您现在有了一个可以工作的本地API和一个使用用户名和密码请求令牌的应用程序。但还有一个问题。令牌当前使用本地状态存储,这意味着它存储在JavaScript内存中。如果您打开新窗口、选项卡,甚至只是刷新页面,您将丢失令牌,用户将需要重新登录。这将在下一步中解决。

在本步骤中,您为应用程序创建了一个本地API和一个登录页面。您了解了如何创建节点服务器以发送令牌,以及如何从登录组件调用服务器并存储令牌。在下一步中,您将学习如何存储用户令牌,以便会话在页面刷新或选项卡中保持不变。

第三步-将用户令牌与会话存储本地存储存储在一起

在本步骤中,您将存储用户令牌。您将实现不同的令牌存储选项,并了解每种方法的安全含义。最后,您将了解在用户打开新选项卡或关闭会话时,不同的方法将如何改变用户体验。

在本步骤结束时,您将能够根据应用程序的目标选择存储方法。

有几种存储令牌的选项。每一种选择都有成本和好处。简而言之,这些选项包括:存储在JAVASCRIPT内存中、存储在sessionStorage,中、存储在localStorage,中和存储在cookie.中主要的权衡是安全问题。存储在当前应用程序内存之外的任何信息都容易受到跨站点脚本(Xss)attacks.]的攻击危险在于,如果恶意用户能够将代码加载到您的应用程序中,它就可以访问本地存储会话存储和您的应用程序也可以访问的任何Cookie。非内存存储方法的好处是您可以减少用户需要登录的次数,以创建更好的用户体验。

本教程将介绍essionStoragelocalStorage,因为它们比使用cookie更现代。

会话存储

要测试内存外存储的好处,请将内存中存储转换为essionStorage。打开App.js

1nano src/components/App/App.js

移除对useState的调用,创建两个名为setTokengetToken的新函数。然后调用getToken,将结果赋给一个名为token的变量:

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4
 5import './App.css';
 6import Dashboard from '../Dashboard/Dashboard';
 7import Login from '../Login/Login';
 8import Preferences from '../Preferences/Preferences';
 9
10function setToken(userToken) {
11}
12
13function getToken() {
14}
15
16function App() {
17  const token = getToken();
18
19  if(!token) {
20    return <Login setToken={setToken} />
21  }
22
23  return (
24    <div className="wrapper">
25     ...
26    </div>
27  );
28}
29
30export default App;

由于您使用的是相同的函数和变量名,因此不需要更改Login组件或App组件的其余部分中的任何代码。

setToken内部,使用setItem方法将userToken参数保存为sessionStorage。此方法将键作为第一个参数,将字符串作为第二个参数。这意味着您需要使用JSON.stringify函数将userToken从对象转换为字符串。使用token键和转换后的对象调用setItem

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4
 5import './App.css';
 6import Dashboard from '../Dashboard/Dashboard';
 7import Login from '../Login/Login';
 8import Preferences from '../Preferences/Preferences';
 9
10function setToken(userToken) {
11  sessionStorage.setItem('token', JSON.stringify(userToken));
12}
13
14function getToken() {
15}
16
17function App() {
18  const token = getToken();
19
20  if(!token) {
21    return <Login setToken={setToken} />
22  }
23
24  return (
25    <div className="wrapper">
26      ...
27    </div>
28  );
29}
30
31export default App;

保存文件。当您这样做时,浏览器将重新加载。如果你输入用户名和密码并提交,浏览器仍会呈现登录页面,但如果你查看浏览器控制台工具,你会发现令牌存储在essionStorage中。这张图片来自火狐,),但你会在Chrome或其他现代浏览器)上找到同样的结果。

sessionStorage中的令牌

现在,您需要检索令牌以呈现正确的页面。在getToken函数内部,调用sessionStorage.getItem。该方法以key为参数,返回字符串值。使用JSON.parse将字符串转换为Object,然后返回token的值:

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4import './App.css';
 5import Dashboard from '../Dashboard/Dashboard';
 6import Login from '../Login/Login';
 7import Preferences from '../Preferences/Preferences';
 8
 9function setToken(userToken) {
10  sessionStorage.setItem('token', JSON.stringify(userToken));
11}
12
13function getToken() {
14  const tokenString = sessionStorage.getItem('token');
15  const userToken = JSON.parse(tokenString);
16  return userToken?.token
17}
18
19function App() {
20  const token = getToken();
21
22  if(!token) {
23    return <Login setToken={setToken} />
24  }
25
26  return (
27    <div className="wrapper">
28      ...
29    </div>
30  );
31}
32
33export default App;

在访问token属性时需要使用可选的链运算符-?.,因为当您第一次访问应用程序时,sessionStorage.getItem(‘Token’)的值将为unfined。如果您尝试访问某个属性,则会生成错误。

保存并关闭该文件。在这种情况下,您已经存储了一个令牌,因此当浏览器刷新时,您将导航到个人主页:

仪表盘

通过在开发者工具的存储 选项卡中删除令牌,或者在开发者控制台中输入sessionStorage.lear()来清除令牌。

现在有个小问题。当您登录时,浏览器会保存令牌,但您仍会看到登录页面。

存储的令牌仍未记录in

问题是你的代码永远不会提醒React令牌检索成功。您仍然需要设置一些状态,以便在数据更改时触发重新渲染。像React中的大多数问题一样,有多种方法可以解决它。最优雅和可重用的方法之一是创建自定义Hook。

创建自定义令牌钩子

自定义钩子是包装自定义逻辑的函数。自定义钩子通常将一个或多个内置的反应钩子与自定义实现包装在一起。自定义Hook的主要优点是您可以从组件中移除实现逻辑,并且可以跨多个组件重用它。

按照惯例,自定义钩子以关键字use* 开头。

打开app目录下名为useToken.js的新文件:

1nano src/components/App/useToken.js

这将是一个小钩子,如果您直接在App.js中定义它就可以了。但是将定制的Hook移到不同的文件中将显示Hook如何在组件外部工作。如果开始跨多个组件重用此钩子,则可能还需要将其移动到单独的目录。

useToken.js内,从react导入useState。请注意,您不需要导入React,因为您的文件中没有JSX。创建并导出一个名为useToken的函数。在此函数内部,使用useState钩子创建token状态和setToken函数:

1[label auth-tutorial/src/components/App/useToken.js]
2import { useState } from 'react';
3
4export default function useToken() {
5  const [token, setToken] = useState();
6
7}

接下来,将getToken函数复制到useHook中,并将其转换为function,,因为您将其放在useToken中。您可以将该函数保留为标准的命名函数,但如果顶级函数是标准函数,而内部函数是箭头函数,则会更容易阅读。然而,每支球队都将是不同的。选择一种风格,并坚持下去。

getToken放在状态声明之前,然后用getToken初始化useState。这将获取令牌并将其设置为初始状态:

 1[label auth-tutorial/src/components/App/useToken.js]
 2import { useState } from 'react';
 3
 4export default function useToken() {
 5  const getToken = () => {
 6    const tokenString = sessionStorage.getItem('token');
 7    const userToken = JSON.parse(tokenString);
 8    return userToken?.token
 9  };
10  const [token, setToken] = useState(getToken());
11
12}

接下来,从App.js复制setToken函数。转换为箭头函数,并将新函数命名为saveToken。除了将令牌保存到会话存储之外,还可以通过调用setToken将令牌保存到状态:

 1[label auth-tutorial/src/components/App/useToken.js]
 2
 3import { useState } from 'react';
 4
 5export default function useToken() {
 6  const getToken = () => {
 7    const tokenString = sessionStorage.getItem('token');
 8    const userToken = JSON.parse(tokenString);
 9    return userToken?.token
10  };
11
12  const [token, setToken] = useState(getToken());
13
14  const saveToken = userToken => {
15    sessionStorage.setItem('token', JSON.stringify(userToken));
16    setToken(userToken.token);
17  };
18}

最后,返回一个包含tokensaveToken设置为setToken属性名的对象。这将为组件提供相同的接口。您也可以将值作为数组返回,但是如果您在另一个组件中重用对象,则对象将使用户有机会仅解构他们想要的值。

 1[label auth-tutorial/src/components/App/useToken.js]
 2import { useState } from 'react';
 3
 4export default function useToken() {
 5  const getToken = () => {
 6    const tokenString = sessionStorage.getItem('token');
 7    const userToken = JSON.parse(tokenString);
 8    return userToken?.token
 9  };
10
11  const [token, setToken] = useState(getToken());
12
13  const saveToken = userToken => {
14    sessionStorage.setItem('token', JSON.stringify(userToken));
15    setToken(userToken.token);
16  };
17
18  return {
19    setToken: saveToken,
20    token
21  }
22}

保存并关闭该文件。

接下来,打开App.js

1nano src/components/App/App.js

移除getTokensetToken函数。然后导入useToken,并调用函数来解构setTokentoken的值。您也可以移除useState的导入,因为您不再使用Hook:

 1[label auth-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import { BrowserRouter, Route, Switch } from 'react-router-dom';
 4import './App.css';
 5import Dashboard from '../Dashboard/Dashboard';
 6import Login from '../Login/Login';
 7import Preferences from '../Preferences/Preferences';
 8import useToken from './useToken';
 9
10function App() {
11
12  const { token, setToken } = useToken();
13
14  if(!token) {
15    return <Login setToken={setToken} />
16  }
17
18  return (
19    <div className="wrapper">
20      <h1>Application</h1>
21      <BrowserRouter>
22        <Switch>
23          <Route path="/dashboard">
24            <Dashboard />
25          </Route>
26          <Route path="/preferences">
27            <Preferences />
28          </Route>
29        </Switch>
30      </BrowserRouter>
31    </div>
32  );
33}
34
35export default App;

保存并关闭该文件。当您这样做时,浏览器将刷新,当您登录时,您将立即转到该页面。这是因为您在自定义Hook中调用了useState,这将触发组件重新渲染:

登录immediately

您现在有了一个定制的Hook来将您的令牌存储在会话存储中。现在您可以刷新页面,用户将保持登录状态。但如果您尝试在另一个选项卡中打开该应用程序,该用户将被注销。essionStorage只属于特定的Windows会话。任何数据都将在新选项卡中不可用,并且在关闭活动选项卡时将丢失。如果您想跨页签保存令牌,则需要转换为localStorage

使用localStorage跨Windows保存数据

sessionStorage不同,localStorage即使在会话结束后也会保存数据。这可能更方便,因为它允许用户在没有新登录的情况下打开多个窗口和选项卡,但它确实存在一些安全问题。如果用户共享他们的计算机,即使他们关闭浏览器,他们也将保持登录到应用程序。用户有责任明确注销。下一个用户无需登录即可立即访问应用程序。这是一种风险,但对于某些应用程序来说,这种便利性可能是值得的。

要转换为localStorage,请打开useToken.js

1nano src/components/App/useToken.js

然后将essionStorage的每个引用都更改为localStorage。您调用的方法将是相同的:

 1[label auth-tutorial/src/components/App/useToken.js]
 2import { useState } from 'react';
 3
 4export default function useToken() {
 5  const getToken = () => {
 6    const tokenString = localStorage.getItem('token');
 7    const userToken = JSON.parse(tokenString);
 8    return userToken?.token
 9  };
10
11  const [token, setToken] = useState(getToken());
12
13  const saveToken = userToken => {
14    localStorage.setItem('token', JSON.stringify(userToken));
15    setToken(userToken.token);
16  };
17
18  return {
19    setToken: saveToken,
20    token
21  }
22}

保存文件。当您这样做时,浏览器将刷新。您需要重新登录,因为localStorage中还没有Token,但是重新登录后,当您打开新标签页时,您将保持登录状态。

仍登录到tab

在本步骤中,您使用sessionStoragelocalStorage保存了令牌。您还创建了一个定制的Hook来触发组件重新呈现,并将组件逻辑移动到单独的函数。您还了解了sessionStoragelocalStorage如何影响用户无需登录即可启动新会话的能力。

结论

身份验证是许多应用程序的关键要求。安全顾虑和用户体验的混合可能令人望而生畏,但如果您专注于在正确的时间验证数据和呈现组件,它可能会成为一个轻量级过程。

每种存储解决方案都有明显的优点和缺点。随着应用程序的发展,您的选择可能会发生变化。通过将组件逻辑移到抽象的定制Hook中,您可以在不中断现有组件的情况下进行重构。

如果您想阅读更多Reaction教程,请查看我们的Reaction主题页面,或返回到[如何在React中编程]系列page](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-react-js).

Published At
Categories with 技术
comments powered by Disqus