如何向React应用程序添加登录身份验证

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。

介绍

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

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

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

先决条件

第 1 步 - 构建登录页面

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

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

首先,使用 npm 安装反应路由器。 有两个不同的版本:Web 版本和用于 React Native 的原生版本。 安装网页版:

npm install react-router-dom

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

Output...
+ react-router-dom@5.2.0
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...

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

首先,创建目录:

mkdir src/components/Dashboard
mkdir src/components/Preferences

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

nano src/components/Dashboard/Dashboard.js

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

授权教程/src/components/Dashboard/Dashboard.js

import React from 'react';

export default function Dashboard() {
  return(
    <h2>Dashboard</h2>
  );
}

保存并关闭文件。

Preferences 重复相同的步骤。 打开组件:

nano src/components/Preferences/Preferences.js

添加内容:

授权教程/src/components/Preferences/Preferences.js

import React from 'react';

export default function Preferences() {
  return(
    <h2>Preferences</h2>
  );
}

保存并关闭文件。

现在您已经有了一些组件,您需要导入组件并在 App.js 内部创建路由。 查看教程 How To Handle Routing in React Apps with React Router 以全面介绍 React 应用程序中的路由。

首先,打开 App.js

nano src/components/App/App.js

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

授权教程/src/components/App/App.js

import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

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

授权教程/src/components/App/App.js

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

添加一个围绕 <div>className wrapper 和一个 <h1> 标记作为应用程序的模板。 确保您正在导入 App.css 以便您可以应用样式。

接下来,为 DashboardPreferences 组件创建路线。 添加 BrowserRouter,然后添加一个 Switch 组件作为子组件。 在 Switch 内部,为每个组件添加一个 Route 和一个 path

教程/src/components/App/App.js

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

保存并关闭文件。

最后一步是向主 <div> 添加一些填充,这样您的组件就不会直接位于浏览器的边缘。 为此,您将更改 CSS

打开App.css

nano src/components/App/App.css

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

授权教程/src/components/App/App.css

.wrapper {
    padding: 20px;
}

保存并关闭文件。 当你这样做时,浏览器将重新加载,你会找到你的基本组件:

检查每条路线。 如果你访问 http://localhost:3000/dashboard,你会发现仪表板页面:

您的路线按预期工作,但有一个小问题。 路由 /dashboard 应该是受保护的页面,未经身份验证的用户不应查看。 有不同的方法来处理私人页面。 例如,您可以为登录页面创建一个新路由,并在用户未登录时使用 React Router 进行重定向。 这是一个很好的方法,但是用户会丢失他们的路线并且必须导航回他们最初想要查看的页面。

一个不那么侵入性的选项是生成登录页面而不考虑路由。 使用这种方法,如果没有存储的用户令牌,您将呈现一个登录页面,并且当用户登录时,他们将在最初访问的同一条路线上。 这意味着如果用户访问 /dashboard,他们在登录后仍然会在 /dashboard 路由上。

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

mkdir src/components/Login

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

nano src/components/Login/Login.js

使用提交 <button> 和用户名和密码的 <input> 创建一个基本表单。 请务必将密码的输入类型设置为 password

授权教程/src/components/Login/Login.js

import React from 'react';

export default function Login() {
  return(
    <form>
      <label>
        <p>Username</p>
        <input type="text" />
      </label>
      <label>
        <p>Password</p>
        <input type="password" />
      </label>
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}

有关 React 中表单的更多信息,请查看教程 How To Build Forms in React

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

授权教程/src/components/Login/Login.js

import React from 'react';
import './Login.css';

export default function Login() {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

保存并关闭文件。

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

nano src/components/Login/Login.css

通过添加 flexdisplay 在页面上居中组件,然后将 flex-direction 设置为 column 以垂直对齐元素并添加 [ X157X] 到 center 使组件在浏览器中居中:

授权教程/src/components/Login/Login.css

.login-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
}

有关使用 Flexbox 的更多信息,请参阅我们的 CSS Flexbox 备忘单

保存并关闭文件。

最后,如果没有用户令牌,您需要在 App.js 内渲染它。 打开App.js

nano src/components/App/App.js

Step 3 中,您将探索存储令牌的选项。 现在,您可以使用 useState Hook 将令牌存储在内存中。

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

授权教程/src/components/App/App.js

import React, { useState } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

导入 Login 组件。 如果 token 为假,则添加 条件语句 以显示 Login

setToken 函数传递给 Login 组件:

授权教程/src/components/App/App.js

import React, { useState } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

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

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

在这一步中,您创建了一个包含私有组件和登录组件的应用程序,该组件将在您设置令牌之前一直显示。 如果用户尚未登录到应用程序,您还配置了路由以显示页面并添加检查以在每个路由上显示 Login 组件。

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

第 2 步 — 创建令牌 API

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

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

首先,安装 express。 由于服务器不是最终构建的要求,因此请务必将 安装为 devDependency

您还需要安装 cors。 该库将为所有路由启用跨源资源共享

警告: 不要为生产应用程序中的所有路由启用 CORS。 这可能导致安全漏洞。


npm install --save-dev express cors

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

Output...
+ cors@2.8.5
+ express@4.17.1
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...

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

nano server.js

导入 express,然后通过调用 express() 初始化一个新应用程序并将结果保存到一个名为 app 的变量中:

授权教程/server.js

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

创建 app 后,将 cors 添加为 中间件。 首先,导入 cors,然后通过在 app 上调用 use 方法将其添加到应用程序中:

授权教程/server.js

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

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

/login 路径添加一个处理程序。 使用包含令牌的 JavaScript 对象 调用 res.send

授权教程/server.js

const express = require('express');
const cors = require('cors')
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

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

授权教程/server.js

const express = require('express');
const cors = require('cors')
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

app.listen(8080, () => console.log('API is running on http://localhost:8080/login'));

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

node server.js

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

OutputAPI is running on http://localhost:8080/login

访问 http://localhost:8080/login 你会找到你的 JSON 对象

当您在浏览器中获取令牌时,您正在发出 GET 请求,但是当您提交登录表单时,您将发出 POST 请求。 这不是问题。 当您使用 app.use 设置您的路线时,Express 将同样处理所有请求。 在生产应用程序中,您应该更具体,并且只允许 某些请求方法 用于每个路由。

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

nano src/components/Login/Login.js

在上一步中,您将一个名为 setToken 的新 prop 传递给了 Login 组件。 从新道具中添加 PropTypedestructure 道具对象以拉出 setToken 道具。

授权教程/src/components/Login/Login.js

import React from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
}

接下来,创建一个本地状态来捕获 UsernamePassword。 由于您不需要手动设置数据,因此使 <inputs> 组件不受控制。 你可以在 How To Build Forms in React 中找到关于不受控制的组件的详细信息。

授权教程/src/components/Login/Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)}/>
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)}/>
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

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

创建一个名为 loginUser异步函数 。 该函数将 credentials 作为参数,然后使用 POST 选项调用 fetch 方法:

授权教程/src/components/Login/Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
...

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

授权教程/src/components/Login/Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();

  const handleSubmit = async e => {
    e.preventDefault();
    const token = await loginUser({
      username,
      password
    });
    setToken(token);
  }

  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

注意: 在完整的应用程序中,您需要处理组件在 Promise 解析之前卸载的情况。 查看教程 How To Call Web APIs with the useEffect Hook in React 了解更多信息。


保存并关闭文件。 确保您的本地 API 仍在运行,然后打开浏览器访问 http://localhost:3000/dashboard。

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

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

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

第 3 步 — 使用 sessionStoragelocalStorage 存储用户令牌

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

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

存储令牌有多种选择。 每个选项都有成本和收益。 简而言之,选项是:存储在 JavaScript 内存中,存储在 sessionStorage 中,存储在 localStorage 中,以及存储在 cookie 中。 主要的权衡是安全性。 存储在当前应用程序内存之外的任何信息都容易受到 跨站点脚本 (XSS) 攻击 。 危险在于,如果恶意用户能够将代码加载到您的应用程序中,它可以访问 localStoragesessionStorage 以及您的应用程序也可以访问的任何 cookie。 非内存存储方法的好处是可以减少用户需要登录的次数,从而创造更好的用户体验。

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

会话存储

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

nano src/components/App/App.js

删除对 useState 的调用并创建两个新函数,分别称为 setTokengetToken。 然后调用 getToken 并将结果分配给一个名为 token 的变量:

授权教程/src/components/App/App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
     ...
    </div>
  );
}

export default App;

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

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

授权教程/src/components/App/App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      ...
    </div>
  );
}

export default App;

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

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

授权教程/src/components/App/App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
  const tokenString = sessionStorage.getItem('token');
  const userToken = JSON.parse(tokenString);
  return userToken?.token
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      ...
    </div>
  );
}

export default App;

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

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

通过在开发人员工具的 Storage 选项卡中删除令牌或在开发人员控制台中键入 sessionStorage.clear() 来清除令牌。

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

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

创建自定义令牌挂钩

自定义 Hook 是包装自定义逻辑的函数。 自定义 Hook 通常包含一个或多个内置 React Hook 以及自定义实现。 自定义 Hook 的主要优点是您可以从组件中删除实现逻辑,并且可以跨多个组件重用它。

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

App 目录中打开一个名为 useToken.js 的新文件:

nano src/components/App/useToken.js

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

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

授权教程/src/components/App/useToken.js

import { useState } from 'react';

export default function useToken() {
  const [token, setToken] = useState();

}

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

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

授权教程/src/components/App/useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };
  const [token, setToken] = useState(getToken());

}

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

授权教程/src/components/App/useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };
}

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

授权教程/src/components/App/useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

保存并关闭文件。

接下来,打开App.js

nano src/components/App/App.js

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

授权教程/src/components/App/App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import useToken from './useToken';

function App() {

  const { token, setToken } = useToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

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

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

使用 localStorage 跨 Windows 保存数据

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

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

nano src/components/App/useToken.js

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

授权教程/src/components/App/useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = localStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    localStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

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

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

结论

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

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

如果您想阅读更多 React 教程,请查看我们的 React 主题页面 ,或返回 如何在 React.js 系列页面中编码