作者选择知识共享接受捐赠,作为WRITE FOR DIRECTIONS计划)的一部分。
简介
许多Web应用程序是公共和私有页面的混合。公共页面对任何人都可用,而私人页面需要用户登录。您可以使用_authentication_来管理哪些用户可以访问哪些页面。您的React应用程序将需要处理用户在登录之前尝试访问私有页面的情况,并且您需要在他们成功认证后保存登录信息。
在本教程中,您将使用基于令牌的身份验证系统创建一个Reaction应用程序。您将创建一个返回用户令牌的模拟API,构建一个获取令牌的登录页面,并在不重新路由用户的情况下检查身份验证。如果用户未通过身份验证,您将为他们提供登录的机会,然后允许他们在不导航到专用登录页面的情况下继续。在构建应用程序时,您将探索存储令牌的不同方法,并了解每种方法的安全性和体验权衡。本教程将重点介绍在LocalStorage
和sessionStorage
.中存储令牌
在本教程结束时,您将能够向Reaction应用程序添加身份验证,并将登录和令牌存储策略集成到一个完整的用户工作流中。
<$>[信息] 需要快速部署Reaction项目吗?查看DigitalOcean App Platform并在几分钟内直接从GitHub部署Reaction项目。 <$>
前提条件
- 您需要一个运行Node.js的开发环境;本教程在Node.js版本10.22.0和npm版本6.14.6上进行了测试。要在macOS或Ubuntu 18.04上安装此版本,请按照如何在macOS上安装Node.js并创建本地开发环境中的步骤或[如何在Ubuntu 18.04上安装Node.js](https://andsky.com/tech/tutorials/how-to-install-node-js-on-ubuntu-18 - 04)中的 ** 使用PPA安装 部分进行操作。
- **使用Create React App设置的React开发环境,删除了不必要的样板文件。要进行设置,请按照如何管理React类组件的状态教程中的 ** Step 1 - Creating an Empty Project** 进行操作。本教程将使用
auth-tutorial
作为项目名称。 - 您将使用React从API获取数据。您可以在如何在React中使用useEffect Hook调用Web API中了解如何使用API。
- 您还需要了解JavaScript、HTML和CSS的基本知识,您可以在我们的如何使用HTML构建网站系列、如何使用CSS设计HTML样式和如何使用JavaScript编写代码中找到这些知识。
第一步-构建登录页面
在本步骤中,您将为应用程序创建一个登录页。您将首先安装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...
接下来,创建两个名为Dashboard
和Pferences
的components作为私有页面。这些组件将代表用户在成功登录到应用程序之前不应看到的组件。
首先,创建目录:
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
然后通过添加以下突出显示的代码导入Dashboard
和Pferences
:
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
导入BrowserRouter
、Switch
、Route
:
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;
添加一个className
为wrapper
和<h1>
标签的<div>
,作为应用程序的模板。请确保您正在导入App.css
,以便可以应用样式。
接下来,为Dashboard
和Pferences
组件创建路由。添加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 添加一些填充
打开App.css
:
1nano src/components/App/App.css
将内容替换为20px
的padding
的类.wrapper
:
1[label auth-tutorial/src/components/App/App.css]
2.wrapper {
3 padding: 20px;
4}
保存并关闭该文件。当您执行此操作时,浏览器将重新加载,您将找到基本组件:
检查每条路线。如果您访问http://localhost:3000/dashboard
,,您将看到仪表板页面:
您的路由工作正常,但有一个小问题。路由/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>
中,className
为login-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
将组件在页面居中,方法是添加flex
的display
,然后将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 中,您将探索存储令牌的选项。目前,您可以使用useState
Hook.将令牌存储在内存中
从react
导入useState
,然后调用useState
,并将返回值设置为token
和setToken
:
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.
当您在浏览器中获取令牌时,您正在发出一个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。
创建名为loginUser
的async
function。该函数将以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
的表单提交处理程序,它将使用username
和password
调用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。非内存存储方法的好处是您可以减少用户需要登录的次数,以创建更好的用户体验。
本教程将介绍essionStorage
和localStorage
,因为它们比使用cookie更现代。
会话存储
要测试内存外存储的好处,请将内存中存储转换为essionStorage
。打开App.js
:
1nano src/components/App/App.js
移除对useState
的调用,创建两个名为setToken
和getToken
的新函数。然后调用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或其他现代浏览器)上找到同样的结果。
中的令牌
现在,您需要检索令牌以呈现正确的页面。在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()
来清除令牌。
现在有个小问题。当您登录时,浏览器会保存令牌,但您仍会看到登录页面。
问题是你的代码永远不会提醒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}
最后,返回一个包含token
和saveToken
设置为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
移除getToken
和setToken
函数。然后导入useToken
,并调用函数来解构setToken
和token
的值。您也可以移除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
,这将触发组件重新渲染:
您现在有了一个定制的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,但是重新登录后,当您打开新标签页时,您将保持登录状态。
在本步骤中,您使用sessionStorage
和localStorage
保存了令牌。您还创建了一个定制的Hook来触发组件重新呈现,并将组件逻辑移动到单独的函数。您还了解了sessionStorage
和localStorage
如何影响用户无需登录即可启动新会话的能力。
结论
身份验证是许多应用程序的关键要求。安全顾虑和用户体验的混合可能令人望而生畏,但如果您专注于在正确的时间验证数据和呈现组件,它可能会成为一个轻量级过程。
每种存储解决方案都有明显的优点和缺点。随着应用程序的发展,您的选择可能会发生变化。通过将组件逻辑移到抽象的定制Hook中,您可以在不中断现有组件的情况下进行重构。
如果您想阅读更多Reaction教程,请查看我们的Reaction主题页面,或返回到[如何在React中编程]系列page](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-react-js).