如何利用仅 HTTP Cookie 保护 React 应用程序免受 XSS 攻击

作者选择了 自由和开源基金作为 写给捐款计划的一部分接受捐款。

介绍

基于代码的身份验证可以保护具有公共和私有资产组合的网络应用程序。 访问私有资产需要用户成功身份验证,通常通过提供用户名和秘密数据,只有用户知道。 成功的身份验证会返回一个代币,只要用户决定保持身份验证,所以用户可以提供代币,而不是在每次访问特权资产时需要重新身份验证。 代币使用会提出存储代币安全的关键问题。 代币可能会被存储在浏览器存储中,使用 Window.localStorage或 [Window.sessionStorage(LINK1)]属性,但这种方法容易受到跨网站脚本([XSSL(INK2)])攻击,因为本地的会话和存储在运行相同的

在本教程中,您将创建一个 React 应用程序,并模仿 API,该应用程序在本地 Docker 容器中实施基于代币的身份验证系统,以便在平台之间进行一致的测试。您将通过使用浏览器存储的代币式身份验证来开始实施,然后您将利用此设置进行反射式跨站点脚本攻击,以了解使用浏览器存储时存在的安全漏洞,以保持机密信息。

到本教程结束时,您将了解实施基于代币的有效身份验证系统以及 React 和 Node 网页应用程序所需的安全考虑,本教程的代码可在 DigitalOcean Community GitHub中找到。

前提条件

要完成本教程,您将需要以下内容:

  • [Docker] (https://www.docker.com/)集装箱内的地方发展环境,您将在步骤1中准备。 您可以直接 [安装 Docker] (https://docs.docker.com/engine/install/] 或遵循关于 [如何在 Ubuntu 22.04 上安装和使用 Docker (https://andsky.com/tech/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04).
  • 一个测试和开发应用程序的浏览器. 此教程使用 [Firefox浏览器] (https://www.mozilla.org/en-US/firefox/new/). 使用不同的浏览器可能会有出乎意料的结果. () ) * 与React合作的熟悉性很有帮助. 您可以参考 如何用 使用效果` 调用网络 API Hook in React (https://andsky.com/tech/tutorials/how-to-call-web-apis-with-the-useeffect-hook-in-react),为从API获取数据所需要. 也可以指: [如何将登录认证添加到 react Application (https://andsky.com/tech/tutorials/how-to-add-login-authentication-to-react-applications#step-3-%E2%80%94-storing-a-user-token-with-sessionstorage-and-localstorage) 对于初级认证系统的帮助. ( ( )* 您将创建和操纵 HTTP 唯一的 cookie 。 有关饼干的更多信息,请参见 [What Are Cookies & How to Work with Them 使用JavaScript] (https://andsky.com/tech/tutorials/js-what-are-cookies) 和 [How To use JSON Web Tokens (JWTs) in Express.js] (https://andsky.com/tech/tutorials/nodejs-jwt-expressjs).
  • 关于JavaScript, HTML,和CSS的知识. 参见如何用 HTML 系列构建网站,如何用 CSS 构建 HTML 样式,并参见 [JavaScript 中如何使用代码(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-javascript) (_). (英语)

步骤 1 – 准备一个Docker容器进行开发

在此步骤中,您将为开发目的设置 Docker 容器. 您将开始创建一个 Dockerfile,其中包含构建图像的说明,以创建您的容器。

在您的主目录中创建并打开名为Dockerfile的文件,使用nano或您喜爱的编辑器:

1nano Dockerfile

将下列行代码放入其内部:

 1[label Dockerfile]
 2FROM node:18.7.0-bullseye
 3
 4RUN apt update -y \
 5    && apt upgrade -y \
 6    && apt install -y vim nano \
 7    && mkdir /app
 8
 9WORKDIR /app
10
11CMD [ "tail", "-f", "/dev/null" ]

FROM行使用Dockerhub的预先构建的node:18.7.0-bullseye创建了您的图像的基础,该图像配备了所需的NodeJS依赖性,这将简化您的安装过程。

「RUN」行更新和升级包,此行还会安装您可能需要的其他包。

CMD线定义了容器内部运行的主要过程,确保容器继续运行,以便您可以连接并使用它进行开发。

保存并关闭文件。

使用docker build命令创建 Docker 图像,将path_to_your_dockerfile替换为您的 Dockerfile 路径:

1docker build -f /path_to_your_dockerfile --tag jwt-tutorial-image .

您的 Dockerfile 路径将被传送到 -f 选项,以表示您将构建图像的文件路径. 您使用 --tag 选项来标记此构建,这使您能够以读者友好的名称(在这种情况下, jwt-tutorial-image)后来参考它。

运行构建命令后,您将看到类似于此的输出:

1[secondary_label Output]
2...
3 => => writing image sha256:1cf8f3253e430cba962a1d205d5c919eb61ad106e2933e33644e0bc4e2cdc433 0.0s 
4 => => naming to docker.io/library/jwt-tutorial-image

运行图像作为一个容器,使用以下命令:

1docker run -d -p 3000:3000 -p 8080:8080 --name jwt-tutorial-container jwt-tutorial-image

「-d」旗将容器运行在 detached 模式中,因此您可以通过单独的终端会话连接到它。

<$>[注] 注: 如果您宁愿使用您使用运行 Docker 容器的相同终端进行开发,请将 -d 标志替换为 -it,这将立即为您提供在容器内运行的交互式终端。

p旗将转发您的容器的端口30008080。这些端口分别将前端和后端应用程序服务于您的主机的本地主机网络,以便您可以使用本地浏览器测试您的应用。

注意: 如果您的主机目前使用30008080端口,则需要停止使用这些端口的应用程序,否则Docker在尝试转发端口时会发出错误。

您还可以使用P标志将您的集装箱端口转发到机器的本地网络中未使用的端口. 如果您使用P标志而不是绘制特定端口,则需要运行docker network inspect your_container_name,以了解哪些开发集装箱端口被绘制到哪些本地端口。

您也可以使用 Remote Containers插件连接到 VSCode。

在单独的终端会话中,运行此命令连接到容器:

1[environment second]
2docker exec -it jwt-tutorial-container /bin/bash

您将在容器标签中看到这样的连接,表示您已连接:

1[environment second]
2[secondary_label Output]
3root@d7e051c96368:/app#

在此步骤中,您将设置预建的 Docker 图像并连接到您将用于开发的容器。

步骤 2 – 建立您的前端应用程序的基础

在此步骤中,您将初始化您的 React 应用程序并配置应用程序管理与一个 ecosystem.config.js 文件。

连接到容器后,使用mkdir命令创建应用程序的目录,然后使用cd命令进入新创建的目录:

1[environment second]
2mkdir /app/jwt-storage-tutorial
3cd /app/jwt-storage-tutorial

然后使用 npx命令运行Create-react-app二进制以初始化一个新的React项目,该项目将作为您的Web应用程序的前端:

1[environment second]
2npx create-react-app front-end

create-react-app二进制将一个裸体的React应用程序与一个README文件进行初始化,用于开发和测试该应用程序,以及一些广泛使用的依赖,包括react-scripts,react-domjest

当被要求继续安装时,输入y

您将看到create-react-app呼叫的此输出:

 1[environment second]
 2[secondary_label Output]
 3...
 4
 5Success! Created front-end at /home/nodejs/jwt-storage-tutorial/front-end
 6Inside that directory, you can run several commands:
 7
 8  yarn start
 9    Starts the development server.
10
11  yarn build
12    Bundles the app into static files for production.
13
14  yarn test
15    Starts the test runner.
16
17  yarn eject
18    Removes this tool and copies build dependencies, configuration files
19    and scripts into the app directory. If you do this, you cant go back!
20
21We suggest that you begin by typing:
22
23  cd front-end
24  yarn start
25
26Happy hacking!

您的输出可能与Create-react-app的不同版本略有不同。

您已经准备好启动一个开发实例,并开始在您的新 React 应用程序上工作。

要运行该应用程序,您将使用 PM2 流程管理器

1[environment second]
2npm install pm2 -g

根据您登录的用户的权限,您可能需要使用sudo命令在全球范围内安装软件包。

PM2在应用程序的开发和生产阶段提供了几个优点,例如,PM2有助于您在开发过程中保持应用程序的不同组件在后台运行。您还可以使用PM2用于生产中的运营需求,例如实施部署模型以对生产应用程序进行最小停机时间的修补。

安装的输出将类似于如下:

1[environment second]
2[secondary_label Output]
3added 183 packages, and audited 184 packages in 2m
412 packages are looking for funding
5  run `npm fund` for details
6found 0 vulnerabilities -->

若要使用 PM2 流程管理器运行您的应用程序,请进入您的 React 项目目录,并使用nano或您偏好的编辑器创建名为ecosystem.config.js的文件:

1[environment second]
2cd front-end
3nano ecosystem.config.js

ecosystem.config.js文件将为 PM2 流程管理器提供如何运行应用程序的配置。

将以下代码添加到新创建的 ecosystem.config.js 文件中:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/ecosystem.config.js]
 3module.exports = {
 4  apps: [
 5    {
 6      name: 'front-end',
 7      cwd: '/app/jwt-storage-tutorial/front-end',
 8      script: 'npm',
 9      args: 'run start',
10      env: {
11        PORT: 3000
12      },
13    },
14  ],
15};

在这里,您可以使用 PM2 流程管理器定义一个新的应用程序配置。名称配置参数允许您在 PM2 流程表中选择一个过程的名称,以便轻松识别。cwd参数设置了您要运行的项目的根目录。脚本args参数允许您选择运行您的程序的命令行工具。最后,env参数允许您通过一个 JSON 对象来为您的应用程序设置所需的环境变量。

保存和退出文件。

使用此命令来检查当前正在运行的 PM2 管理器的哪些流程:

1[environment second]
2pm2 list

在这种情况下,您目前没有在 PM2 上运行任何过程,因此您可以获得以下输出:

1[environment second]
2[secondary_label Output]
3┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
4│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
5└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

如果您正在运行命令,并且需要重新设置新表格的流程管理器,请运行此命令:

1[environment second]
2pm2 delete all

现在,使用 PM2 流程管理器启动您的应用程序,并在ecosystem.config.js文件中指定的配置:

1[environment second]
2pm2 start ecosystem.config.js

您将在终端上看到类似的输出:

1[environment second]
2[secondary_label Output]
3┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
4│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
5├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
6│ 0  │ front-end          │ fork     │ 0    │ online    │ 0%       │ 33.6mb   │
7└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

您可以使用停止启动命令以及重新启动启动或重新启动命令来控制 PM2 过程的活动。

您可以通过浏览您喜爱的浏览器中的 http://localhost:3000 来查看该应用程序。默认的 React 欢迎页面将显示:

Screencapture of the React application's initial startup display in the browser

最后,安装 react-router的版本 5.2.0 用于客户端路由:

1[environment second]
2npm install [email protected]

安装完成后,您将收到以下消息的变体:

 1[environment second]
 2[secondary_label Output]
 3...
 4
 5added 13 packages, and audited 1460 packages in 7s
 6
 7205 packages are looking for funding
 8  run `npm fund` for details
 9
106 high severity vulnerabilities
11
12To address all issues (including breaking changes), run:
13  npm audit fix --force
14
15Run `npm audit` for details.

在此步骤中,您将在 Docker 容器中设置 React 应用程序的骨骼,接下来,您将为您的应用程序构建页面,后者将用于测试 XSS 攻击。

步骤三:创建一个Login页面

在此步骤中,您将为您的应用程序创建登录页面. 您将使用组件来代表具有私有和公共资产的应用程序. 然后,您将实现登录页面,用户将验证自己,以获得访问网站上的私有资产的权限。

首先,您将创建主页登录页面,然后您将创建一个订阅源组件,代表一个私人页面,只有已登录的用户才能查看。

首先,创建一个组件目录,存储您的应用程序的所有组件:

1[environment second]
2mkdir src/components

然后,在组件目录中创建并打开一个名为SubscriberFeed.js的新文件:

1[environment second]
2nano src/components/SubscriberFeed.js

SubscriberFeed.js文件中,添加这些行与<h2>标签,其中包含组件的标题:

1[environment second]
2[label jwt-storage-tutorial/front-end/src/components/SubscriberFeed.js]
3import React from 'react';
4
5export default () => {
6  return(
7    <h2>Subscriber Feed</h2>
8  );
9}

保存并关闭文件。

接下来,您将导入SubscriberFeed组件到App.js文件中,创建路径,使该组件能够被用户访问。

1[environment second]
2nano src/App.js

添加以下突出行以从反响路由器门包中导入BrowserRouter,SwitchRoute组件:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { BrowserRouter, Route, Switch } from 'react-router-dom';
 7
 8function App() {
 9  return (
10    <div className="App">
11      <header className="App-header">
12        <img src={logo} className="App-logo" alt="logo" />
13        <p>
14          Edit <code>src/App.js</code> and save to reload.
15        </p>
16        <a
17          className="App-link"
18          href="https://reactjs.org"
19          target="_blank"
20          rel="noopener noreferrer"
21        >
22          Learn React
23        </a>
24      </header>
25    </div>
26  );
27}
28
29export default App;

您将使用这些来设置您的Web应用程序中的路由。

接下来,添加突出的行,以导入您刚刚创建的订阅源组件:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { BrowserRouter, Route, Switch } from 'react-router-dom';
 7import SubscriberFeed from "./components/SubscriberFeed";
 8
 9function App() {
10  return (
11    <div className="App">
12      <header className="App-header">
13        <img src={logo} className="App-logo" alt="logo" />
14        <p>
15          Edit <code>src/App.js</code> and save to reload.
16        </p>
17        <a
18          className="App-link"
19          href="https://reactjs.org"
20          target="_blank"
21          rel="noopener noreferrer"
22        >
23          Learn React
24        </a>
25      </header>
26    </div>
27  );
28}
29
30export default App;

您现在已经准备好创建您的主要应用程序和您的网页的路线。

src/App.js中,删除返回的 JSX行(在返回关键字后包含的关节内的一切)并用突出的行替换它们:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { BrowserRouter, Route, Switch } from 'react-router-dom';
 7import SubscriberFeed from "./components/SubscriberFeed";
 8
 9function App() {
10  return(
11    <div className="App">
12      <h1 className="App-header">
13        JWT-Storage-Tutorial Application
14      </h1>
15    </div>
16  );
17}
18
19export default App;

div标签具有应用程序className属性,其中包含与应用程序名称的<h1>标签。

<h1>标签下方,添加一个BrowserRouter组件,该组件使用Switch组件包装包含SubscriberFeed组件的Route组件:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { BrowserRouter, Route, Switch } from 'react-router-dom';
 7import SubscriberFeed from "./components/SubscriberFeed";
 8
 9function App() {
10  return(
11    <div className="App">
12      <h1 className="App-header">
13        JWT-Storage-Tutorial Application
14      </h1>
15      <BrowserRouter>
16        <Switch>
17          <Route path="/subscriber-feed">
18            <SubscriberFeed />
19          </Route>
20        </Switch>
21      </BrowserRouter>
22    </div>
23  );
24}
25
26export default App;

这些新行允许您定义应用程序的路径.BrowserRouter组件包含您定义的路径.Switch组件确保返回的路径是用户导航的路径的第一个路径,而Route组件定义特定路径名称。

最后,您将使用 CSS 添加插件到您的应用程序中,以便标题和组件以中心和可呈现为准。

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { BrowserRouter, Route, Switch } from 'react-router-dom';
 7import SubscriberFeed from "./components/SubscriberFeed";
 8
 9function App() {
10  return(
11    <div className="App wrapper">
12      <h1 className="App-header">
13        JWT-Storage-Tutorial Application
14      </h1>
15      <BrowserRouter>
16        <Switch>
17          <Route path="/subscriber-feed">
18            <SubscriberFeed />
19          </Route>
20        </Switch>
21      </BrowserRouter>
22    </div>
23  );
24}
25
26export default App;

保存并关闭App.js文件。

打开App.css文件:

1[environment second]
2nano src/App.css

您将在此文件中看到现有的 CSS. 删除文件中的所有内容。

然后,添加以下行来定义包装的风格:

1[environment second]
2[label jwt-storage-tutorial/front-end/src/App.css]
3.wrapper {
4    padding: 20px;
5    text-align: center;
6}

您将wrapper类的text-align属性设置为center以中心应用程序中的文本。

保存并关闭App.css文件。

您可能會看到您的 React 主頁更新與新的風格。 瀏覽 http://localhost:3000/subscriber-feed 查看訂閱者節目,現在可以看到。

Screencapture of the React application with a visible subscriber feed page

路径按预期运行,但所有访问者都可以访问订阅者电源. 为了确保订阅者电源仅可见于已验证的用户,您需要创建一个登录页面,让用户用用户名和密码验证自己。

在您的组件目录中打开新的Login.js文件:

1[environment second]
2nano src/components/Login.js

将以下行添加到新文件中:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3import React from 'react';
 4
 5export default () => {
 6  return(
 7    <div className='login-wrapper'>
 8      <h1>Login</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}

您创建一个表单具有<h1>标签标题,两个输入(用户名密码)和一个提交按钮。

保存并关闭文件。

在项目的根目录中打开App.css文件,以格式化您的登录组件:

1[environment second]
2nano src/App.css

添加下列 CSS 行来格式化login-wrapper类:

1[environment second]
2[label jwt-storage-tutorial/front-end/src/App.css]
3...
4
5.login-wrapper {
6    display: flex;
7    flex-direction: column;
8    align-items: center;
9}

您将页面上的组件以显示属性为flex对齐项目属性为中心为中心,然后将flex-方向设置为,该属性将在列中垂直对齐元素。

保存并关闭文件。

最后,您将使用 useState HookLogin组件渲染到App.js内部,以便将代币存储在内存中。

1[environment second]
2nano src/App.js

将突出的行添加到文件中:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState } from 'react'
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11
12function App() {
13  const [token, setToken] = useState();
14
15  if (!token) {
16    return <Login setToken={setToken} />
17  }
18
19  return(
20    <div className="App wrapper">
21      <h1 className="App-header">
22        JWT-Storage-Tutorial Application
23      </h1>
24      <BrowserRouter>
25        <Switch>
26          <Route path="/subscriber-feed">
27            <SubscriberFeed />
28          </Route>
29        </Switch>
30      </BrowserRouter>
31    </div>
32  );
33}
34
35export default App;

首先,您从响应包中导入useState链接。

您还会创建一个新的代币状态变量来存储在登录过程中收集的代币信息。在步骤5中,您将通过使用浏览器存储来改进此设置,以保持身份验证状态。

你还导入了Login组件,如果token的值是false,它会显示登录页面。if声明声明说,如果代码是false,用户将被要求登录,如果他们没有身份验证。你将setToken函数传递给Login组件作为一个支持。

保存并关闭文件。

然后,刷新您的应用程序页面以加载新建的登录页面. 由于目前没有实现令牌设置的功能,该应用程序只会显示登录页面:

Screencapture of the React application login form showing username and password input boxes

在此步骤中,您更新了您的应用程序的登录页面和私人组件,这些组件将被保护免受未经授权的用户侵害,直到他们登录。

在下一步中,您将使用NodeJS创建一个新的后端应用程序,以及一个新的登录路径,以便在前端应用程序上调用身份验证令牌。

步骤 4 – 创建一个 Token API

在此步骤中,您将创建一个 Node 服务器作为您在前一步设置的前端 React 应用程序的后端。 您将使用 Node 服务器创建并提供一个 API,在成功前端用户身份验证时返回身份验证标记。 在此步骤的结束时,您的应用程序将有一个工作登录页面,只有成功身份验证后可用的私人资源,以及一个后端服务器应用程序,允许通过 API 呼叫进行身份验证。

您将使用 Express框架构建服务器. 您将使用 cors 包,为所有路径启用 cross-origin resource sharing 然后您可以测试和开发您的应用程序而无需 CORS 错误。

<$>[warning] 警告: CORS 在开发环境中被启用用于教学目的,但是,在生产应用程序中的所有路径上启用CORS会导致安全漏洞。

创建并移动到一个名为后端的新目录,该目录将容纳您的 Node 项目:

1[environment second]
2mkdir /app/jwt-storage-tutorial/back-end
3cd /app/jwt-storage-tutorial/back-end

在新目录中,启动 Node 项目:

1[environment second]
2npm init -y

init命令告诉npm命令行实用程序在运行命令的目录中创建一个新的节点项目。-y旗用于所有交互式命令行工具在创建新项目时提出的初始化问题的默认值。

 1[environment second]
 2[secondary_label Output]
 3Wrote to /home/nodejs/jwt-storage-tutorial/back-end/package.json:
 4
 5{
 6  "name": "back-end",
 7  "version": "1.0.0",
 8  "description": "",
 9  "main": "index.js",
10  "scripts": {
11    "test": "echo \"Error: no test specified\" && exit 1"
12  },
13  "keywords": [],
14  "author": "",
15  "license": "ISC"
16}

接下来,在后端项目目录中安装快递核心模块:

1[environment second]
2npm install express cors

下列输出的一些变异将出现在终端中:

1[environment second]
2[secondary_label Output]
3added 59 packages, and audited 60 packages in 3s
4
57 packages are looking for funding
6  run `npm fund` for details
7
8found 0 vulnerabilities

创建一个新的 index.js 文件:

1[environment second]
2nano index.js

添加以下行来导入express模块,并通过调用express()来初始化新的Express应用程序,并将结果存储在一个变量中以app的名称:

1[environment second]
2[label jwt-storage-tutorial/back-end/index.js]
3const express = require('express');
4const app = express();

接下来,将cors添加到应用程序中为 middleware 与突出的行:

1[environment second]
2[label jwt-storage-tutorial/back-end/index.js]
3const express = require('express');
4const cors = require('cors');
5
6const app = express();
7
8app.use(cors());

您导入cors模块,然后使用use方法将其添加到app对象中。

然后添加突出的行来定义一个/login路径的处理器,该路径会向试图登录的用户返回一个代币:

 1[environment second]
 2[label jwt-storage-tutorial/back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const app = express();
 7
 8app.use(cors());
 9
10app.use('/login', (req, res) => {
11    res.send({
12      token: "This is a secret token"
13    });
14});

您通过app.use()方法定义一条路径的请求处理器. 该路径将允许您从您刚刚建立的前端应用程序发送被验证的用户的用户名和密码. 作为回报,您将为用户提供身份验证代码,以便向后端应用程序进行身份验证的呼叫。

app.use方法的第一个参数是应用程序将接受请求的路径,第二个参数是回调,详细说明如何处理应用程序接收的请求,回调需要两个参数:包含请求数据的req参数和包含响应数据的res参数。

<$>[注] **注:**当用户要求使用后端API登录时,您不会检查传递的凭证的准确性。

最后,添加突出的行,以便使用app.listen函数在端口8080上运行服务器:

 1[environment second]
 2[label jwt-storage-tutorial/back-end/index.js]
 3
 4const express = require('express');
 5const cors = require('cors');
 6
 7const app = express();
 8
 9app.use(cors());
10
11app.use('/login', (req, res) => {
12    res.send({
13      token: "This is a secret token"
14    });
15});
16
17app.listen(8080, () => console.log(`API is active on http://localhost:8080`));

保存并关闭文件。

若要使用 PM2 執行後端應用程式,請建立新的「後端/ecosystem.config.js」檔案:

1[environment second]
2nano ecosystem.config.js

将以下配置代码添加到新创建的 back-end/ecosystem.config.js 文件中:

 1[environment second]
 2[label jwt-storage-tutorial/back-end/ecosystem.config.js]
 3module.exports = {
 4  apps: [
 5    {
 6      name: 'back-end',
 7      cwd: '/app/jwt-storage-tutorial/back-end',
 8      script: 'node',
 9      args: 'index.js',
10      watch: ['index.js']
11    },
12  ],
13};

PM2将与前端应用程序相似的配置参数来管理后端应用程序。

您在配置文件中设置了watch参数,以便每次对文件进行更改时自动重新加载应用程序。watch参数是一个有用的开发功能,因为它更新了浏览器中的结果,因为对代码进行了更改。您不需要对前端应用程序的 watch 参数,因为您使用了react-scripts,默认情况下具有自动重新加载功能。然而,您的后端应用程序将使用node运行时间运行,并且没有默认功能。

保存并关闭文件。

您现在可以使用pm2来运行后端应用程序:

1[environment second]
2pm2 start ecosystem.config.js

您的输出将是以下的一些变量:

 1[environment second]
 2[secondary_label Output]
 3[PM2][WARN] Applications back-end not running, starting...
 4[PM2] App [back-end] launched (1 instances)
 5┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
 6│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
 7├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
 8│ 2  │ back-end           │ fork     │ 0    │ online    │ 0%       │ 24.0mb   │
 9│ 0  │ front-end          │ fork     │ 9    │ online    │ 0%       │ 47.2mb   │
10└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

您将使用 curl来评估您新创建的 API 终端是否正确返回身份验证令牌:

1[environment second]
2curl localhost:8080/login

你应该看到以下结果:

1[environment second]
2[secondary_label Output]
3{"token":"This is a secret token"}

您现在知道您的服务器登录路径会按预期返回代币。

接下来,您将更改您的前端登录组件以使用API。

1[environment second]
2cd ..
3cd front-end/src/components/

打开前端Login.js文件:

1[environment second]
2nano Login.js

添加突出的线条:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3import React, { useRef } from 'react';
 4
 5export default () => {
 6  const emailRef = useRef();
 7  const passwordRef = useRef();
 8
 9  return(
10    <div className='login-wrapper'>
11      <h1>Login</h1>
12      <form>
13        <label>
14          <p>Username</p>
15          <input type="text" ref={emailRef} />
16        </label>
17        <label>
18          <p>Password</p>
19          <input type="password" ref={passwordRef} />
20        </label>
21        <div>
22          <button type="submit">Submit</button>
23        </div>
24      </form>
25    </div>
26  );
27}

您添加了 useRef hook 以跟踪电子邮件和密码输入字段的值。 键入与 useRef hook 相关的输入字段时,将更新引用中的值,然后在按下提交按钮后发送到后端。

接下来,添加突出的行,以创建一个HandleSubmit召回,以便在表单上按下提交按钮时处理:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3import React, { useRef } from 'react';
 4
 5async function loginUser(credentials) {
 6  return fetch('http://localhost:8080/login', {
 7    method: 'POST',
 8    headers: {
 9      'Content-Type': 'application/json'
10    },
11    body: JSON.stringify(credentials)
12  }).then(data => data.json())
13}
14
15export default ({ setToken }) => {
16  const emailRef = useRef();
17  const passwordRef = useRef();
18
19  const handleSubmit = async (e) => {
20    e.preventDefault();
21    const token = await loginUser({
22        username: emailRef.current.value,
23        password: passwordRef.current.value
24    })
25    setToken(token)
26  }
27
28  return(
29    <div className='login-wrapper'>
30      <h1>Login</h1>
31      <form onSubmit={handleSubmit}>
32        <label>
33          <p>Username</p>
34          <input type="text" ref={emailRef} />
35        </label>
36        <label>
37          <p>Password</p>
38          <input type="password" ref={passwordRef} />
39        </label>
40        <div>
41          <button type="submit">Submit</button>
42        </div>
43      </form>
44    </div>
45  );
46}

handleSubmit处理函数内,您呼叫loginUser辅助函数以向先前创建的API的login路径提出收集请求。在事件上调用preventDefault函数转到handleSubmit函数时,意味着提交按钮的默认更新功能没有执行,因此您的应用可以代替调用登录终端并处理用户登录所需的步骤。

保存并关闭文件完成后。

当您在浏览器中检查 Web 应用程序时,您现在可以使用任意的用户名和密码登录。 按下 ** Submit** 按钮将被重定向到您已登录的页面。

在下一步中,您将使用浏览器存储来保持在前端应用程序中收到的代币。

步骤 5 – 通过浏览器存储存储代币

在此步骤中,您将使用 Window.localStorage 属性来存储持续用户会话的身份验证代币,这些代币在用户关闭浏览器或更新网页时不会丢失。

浏览器存储包括两种不同但相似的存储类型: 本地存储会话存储.简而言之,会话存储将持续在每个 tab 会话中的数据,而本地存储将持续在每个 tab 和浏览器会话中的数据。

打开您的前端应用程序的App.js文件:

1[environment second]
2nano /app/jwt-storage-tutorial/front-end/src/App.js

要开始您的浏览器存储集成,添加突出的行,其中定义了两个辅助函数(‘setToken’和‘getToken’),并更改了‘token’变量以使用新实现的函数获得代币:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState } from 'react'
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11
12function setToken(userToken) {
13  localStorage.setItem('token', JSON.stringify(userToken));
14  window.location.reload(false)
15}
16
17function getToken() {
18  const tokenString = localStorage.getItem('token');
19  const userToken = JSON.parse(tokenString);
20  return userToken?.token
21}
22
23function App() {
24  let token = getToken()
25
26  if (!token) {
27    return <Login setToken={setToken} />
28  }
29
30  return(
31    <div className="App wrapper">
32      <h1 className="App-header">
33        JWT-Storage-Tutorial Application
34      </h1>
35      <BrowserRouter>
36        <Switch>
37          <Route path="/subscriber-feed">
38            <SubscriberFeed />
39          </Route>
40        </Switch>
41      </BrowserRouter>
42    </div>
43  );
44}
45
46export default App;

您创建两个辅助函数: setTokengetToken. 在 setToken 中,您使用 localStoragesetItem 函数将辅助函数的 userToken 输入参数绘制到一个名为 token 的密钥。

getToken中,您将使用localStoragegetItem函数来检查token密钥是否存在任何值,您将返回它。

每当用户访问您的网站时,前端将检查浏览器存储中是否有身份验证令牌,并尝试使用已经存在的令牌验证用户,而不是要求他们登录。

保存并关闭文件,然后更新应用程序. 您现在应该能够登录应用程序,更新网页,无需再次登录。

在此步骤中,您使用浏览器存储实现了代币持久性,您将在下一节中利用基于代币的身份验证系统使用浏览器存储。

步骤 6 — 利用 XSS 攻击的浏览器存储

在此步骤中,您将对当前应用程序执行阶段性的 cross-site scripting attack(也称为 XSS 攻击),该攻击将展示使用浏览器存储时存在的安全漏洞,以保持机密信息。

XSS攻击是当今最常见的网络攻击之一。攻击者通常会将恶意脚本注入浏览器,以便在可信的环境中实现代码执行。

XSS攻击对攻击者特别感兴趣,其目的是窃取不怀疑受害者的浏览器存储的内容,因为一个域的浏览器存储完全可以访问与该域相关的任何文档运行的JavaScript代码。

为了指导目的,你会故意让你的应用程序容易受到XSS攻击,通过创建一个名为XSSHelper的组件,可以通过URL查询参数注入代码。

在前端应用程序的组件目录中打开名为XSSHelper.js的新组件:

1[environment second]
2nano /app/jwt-storage-tutorial/front-end/src/components/XSSHelper.js

将以下代码添加到新文件中:

 1[environment second]
 2[label /src/components/XSSHelper.js]
 3import React from 'react';
 4import { useLocation } from 'react-router-dom';
 5
 6export default (props) => {
 7  const search = useLocation().search;
 8  const code = new URLSearchParams(search).get('code');
 9
10  return(
11    <h2>XSS Helper Active</h2>
12  );
13}

您创建一个新的功能组件,该组件导入 useLocation链接,并通过useLocation’链接的搜索属性访问代码`查询参数。

JavaScript 函数 URLSearchParams提供了帮助方法,如 getters 用于与搜索字符串互动。

现在,添加突出的行来导入,并使用useEffect链接来记录查询参数的值:

 1[environment second]
 2[label /src/components/XSSHelper.js]
 3import React, { useEffect } from 'react';
 4import { useLocation } from 'react-router-dom';
 5
 6export default (props) => {
 7  const search = useLocation().search;
 8  const code = new URLSearchParams(search).get('code');
 9
10  useEffect(() => {
11    console.log(code)
12  })
13
14  return(
15    <h2>XSS Helper Active</h2>
16  );
17}

保存并关闭文件。

接下来,您将更改您的App.js文件以返回组件,当用户导航到您的应用程序的xss-helper路线时。

打开App.js文件:

1[environment second]
2nano /app/jwt-storage-tutorial/front-end/src/App.js

添加突出的行来导入,并将XSSHelper组件添加为路线:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState } from 'react'
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11import XSSHelper from './components/XSSHelper'
12
13function setToken(userToken) {
14  localStorage.setItem('token', JSON.stringify(userToken));
15  window.location.reload(false)
16}
17
18function getToken() {
19  const tokenString = localStorage.getItem('token');
20  const userToken = JSON.parse(tokenString);
21  return userToken?.token
22}
23
24function App() {
25  let token = getToken()
26
27  if (!token) {
28    return <Login setToken={setToken} />
29  }
30
31  return(
32    <div className="App wrapper">
33      <h1 className="App-header">
34        JWT-Storage-Tutorial Application
35      </h1>
36      <BrowserRouter>
37        <Switch>
38          <Route path="/subscriber-feed">
39            <SubscriberFeed />
40          </Route>
41          <Route path="/xss-helper">
42            <XSSHelper />
43          </Route>
44        </Switch>
45      </BrowserRouter>
46    </div>
47  );
48}
49
50export default App;

保存并关闭文件。

在浏览器中导航到localhost:3000/xss-helper?code=在这里注入代码 请确保您已登录到应用程序,否则您将无法访问XSSHelper`组件。

点击左键,按 Inspect. 然后导航到 Console 部分. 您将在控制台日志中看到在这里注入代码

Screencapture of the XSS Helper page with the URL query parameter passing to show the <code>'inject code here'</code> message

您现在知道,您可以将 URL 查询参数传递给您的组件。

接下来,您将使用 dangerouslySetInnerHTML 属性设置在 Web 页面文档中传入您的组件的查询参数的值。

<$>[警告] **警告:**在生产环境中使用 dangerouslySetInnerHTML 属性可能会使您的应用程序容易受到 XSS 攻击。

再次打开XSSHelper文件:

1[environment second]
2nano XSSHelper.js

添加突出的线条:

 1[environment second]
 2[label src/components/XSSHelper.js]
 3import React, {useEffect} from 'react';
 4import { useLocation } from 'react-router-dom';
 5
 6export default (props) => {
 7  const search = useLocation().search;
 8  const code = new URLSearchParams(search).get('code');
 9
10  useEffect(() => {
11    console.log(code)
12  })
13
14  return(
15    <>
16      <h2>XSS Helper Active</h2>
17      <div dangerouslySetInnerHTML={{__html: code}} />
18    </>
19  );
20}

您将返回的元素包装在一个空的 JSX 标签(<>... </>)中,以避免与 React 片段工作时语法上违法的多个片段 JSX 返回。

保存并关闭文件。

您现在可以将恶意编写的代码注入到您的组件中,以便在网页上实现代码执行。

你知道,发送到xss-helper路径的代码查询参数的值将直接嵌入到你的应用程序的文档中。你可以将代码查询参数的值设置为具有href属性将自定义JavaScript代码直接传递给浏览器的<a>标签的链接。

导航到您的浏览器中的以下URL:

1localhost:3000/xss-helper?code=<a href="javascript:alert(`You have been pwned`);">Click Me!</a>

在上面的 URL 中,您创建了一个查询参数 XSS 效用量,以显示在网页上读取Click Me!的链接。当用户点击链接时,链接告诉浏览器执行您创建的 JavaScript 代码。

Screencapture of a successful XSS attack that displays the "You have been pwned" pop-up

接下来,导航到您的浏览器中的以下URL:

1localhost:3000/xss-helper?code=<a href="javascript:alert(`Your token object is ${localStorage.getItem('token')}. It has been sent to a malicious server >:)`);">Click Me!</a>

对于本页面,浏览器存储内容可以通过URL查询参数脚本注入来访问攻击者,该脚本通过JavaScript代码读取存储在localStorage中的代码的值。

您必须登录到应用程序,以便代币存在,允许您恶意制作的 URL 显示存储在本地存储中的代币. 当您点击网页上的 Click Me! 链接时,您将收到一个 pop-up 消息,即您的代币已被盗。

Screencapture of a successful XSS attack for stealing the contents of local storage with a pop-up message informing the user of a stolen token

在此步骤中,您使用了许多样本攻击向量之一来实现代码执行. 使用无疑用户的身份验证令牌,恶意攻击者可以假装用户在您的Web应用程序上访问特权网站资产. 从这些测试中,您现在知道在浏览器存储中存储秘密信息,如身份验证令牌,是一种不安全的做法。

接下来,您将使用一种替代的方法来存储机密信息,这些信息将无法访问在文档上运行的脚本,并且免受这种类型的XSS攻击。

在此步骤中,您将使用仅用于 HTTP 的 Cookie 来缓解前一步发现和利用的 XSS 漏洞。

HTTP Cookie是存储在浏览器中的关键值对中信息的片段,通常用于跟踪,个性化或会话管理。

JavaScript 无法通过Document.cookie属性访问仅限 HTTP 的 Cookie,这有助于防止 XSS 攻击以通过恶意代码注入来窃取用户信息。您可以使用Set-Cookie标题(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)为认证客户端设置 Cookie 服务器侧,该 cookie 将可用于每个客户端向服务器提出的请求,然后由服务器用于检查用户的身份验证状态。

要实现仅基于 HTTP 的基于 Cookie 的安全代币存储,您将更新以下文件:

  • 后端 index.js 文件将被修改以实现 login 路径,以便在成功验证时设置 Cookie。 后端还需要两个新路径:一个用于检查用户的身份验证状态,另一个用于退出用户。

这些更改将为您的客户端和服务器代码实现登录、退出和身份验证状态功能。

转到后端目录并安装 cookie-parser,这将允许您在您的Express应用程序中设置和阅读cookie:

1[environment second]
2cd /app/jwt-storage-tutorial/back-end
3npm install cookie-parser

您将看到以下输出的变量:

1[environment second]
2[secondary_label Output]
3...
4added 2 packages, and audited 62 packages in 1s
5
67 packages are looking for funding
7  run `npm fund` for details
8
9found 0 vulnerabilities...

接下来,在后端应用程序中打开index.js:

1[environment second]
2nano /app/jwt-storage-tutorial/back-end/index.js

添加突出的代码以使用要求方法导入新安装的cookie-parser包,并将其用作应用中的中间件:

 1[environment second]
 2[label back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const cookieParser = require('cookie-parser')
 7
 8const app = express();
 9
10app.use(cors());
11app.use(cookieParser())
12
13app.post('/login', (req, res) => {
14    res.send({
15      token: "This is a secret token"
16    });
17});
18
19app.listen(8080, () => console.log('API active on http://localhost:8080'));

您还将配置cors中间软件以绕过 CORS 限制用于开发目的. 在相同的文件中,添加突出的行:

 1[environment second]
 2[label back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const cookieParser = require('cookie-parser')
 7
 8const app = express();
 9
10let corsOptions = {
11  origin: 'http://localhost:3000',
12  credentials: true,
13}
14
15app.use(cors(corsOptions));
16app.use(cookieParser())
17
18app.post('/login', (req, res) => {
19    res.send({
20      token: "This is a secret token"
21    });
22});
23
24app.listen(8080, () => console.log('API active on http://localhost:8080'));

您将Access-Control-Allow-Origin CORS 标题设置为您的前端发送 API 请求的域,使用origin选项在corsOptions对象下。 您还将credentials参数设置为true,该参数告诉前端在每个 API 请求中预计将发送授权令牌。

最后,您将corsOptions配置对象转移到cors中间件对象中。

接下来,您将使用cookie()方法设置用户的cookie代码,该方法由cookie-parser中间软件为您的路由处理器的响应对象提供。

 1[environment second]
 2[label back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const cookieParser = require('cookie-parser')
 7
 8const app = express();
 9
10let corsOptions = {
11  origin: 'http://localhost:3000',
12  credentials: true,
13}
14
15app.use(cors(corsOptions));
16app.use(cookieParser())
17
18app.use('/login', (req, res) => {
19    res.cookie("token", "this is a secret token", {
20      httpOnly: true,
21      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
22      domain: "localhost",
23      sameSite: 'Lax',
24    }).send({
25      authenticated: true,
26      message: "Authentication Successful."});
27});
28
29app.listen(8080, () => console.log('API active on http://localhost:8080'));

在上面的代码块中,您将设置一个具有代币这是一个秘密代币的密钥的cookie。

您将maxAge属性设置为cookie在14天内到期,14天后,cookie将到期,浏览器将需要一个新的身份验证cookie,因此用户需要使用其用户名和密码再次登录。

同一网站属性设置以确保客户端浏览器不会因为CORS或其他安全协议问题而拒绝您的cookies。

现在你有一个路由来登录,你需要一条路由来退出. 添加突出的行来设置退出方法:

 1[environment second]
 2[label back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const cookieParser = require('cookie-parser')
 7
 8const app = express();
 9
10let corsOptions = {
11  origin: 'http://localhost:3000',
12  credentials: true,
13}
14
15app.use(cors(corsOptions));
16app.use(cookieParser())
17
18app.use('/login', (req, res) => {
19    res.cookie("token", "this is a secret token", {
20      httpOnly: true,
21      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
22      domain: "localhost",
23      sameSite: 'Lax',
24    }).send({
25      authenticated: true,
26      message: "Authentication Successful."});
27});
28
29app.use('/logout', (req, res) => {
30  res.cookie("token", null, {
31    httpOnly: true,
32    maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
33    domain: "localhost",
34    sameSite: 'Lax',
35  }).send({
36    authenticated: false,
37    message: "Logout Successful."
38  });
39});
40
41app.listen(8080, () => console.log('API active on http://localhost:8080'));

logout方法与login路径类似,该logout方法将通过将tokencookie设置为null来删除用户作为cookie存储的代币。

最后,添加突出的行,实现一个auth-status路径,允许用户客户端检查用户是否已登录并允许访问私人资产:

 1[environment second]
 2[label back-end/index.js]
 3const express = require('express');
 4const cors = require('cors');
 5
 6const cookieParser = require('cookie-parser')
 7
 8const app = express();
 9
10let corsOptions = {
11  origin: 'http://localhost:3000',
12  credentials: true,
13}
14
15app.use(cors(corsOptions));
16app.use(cookieParser())
17
18app.use('/login', (req, res) => {
19    res.cookie("token", "this is a secret token", {
20      httpOnly: true,
21      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
22      domain: "localhost",
23      sameSite: 'Lax',
24    }).send({
25      authenticated: true,
26      message: "Authentication Successful."});
27});
28
29app.use('/logout', (req, res) => {
30  res.cookie("token", null, {
31    httpOnly: true,
32    maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
33    domain: "localhost",
34    sameSite: 'Lax',
35  }).send({
36    authenticated: false,
37    message: "Logout Successful."
38  });
39});
40
41app.use('/auth-status', (req, res) => {
42  console.log(req.cookies)
43
44  if (req.cookies?.token === "this is a secret token") {
45    res.send({isAuthenticated: true})
46  } else {
47    res.send({isAuthenticated: false})
48  }
49})
50
51app.listen(8080, () => console.log('API active on http://localhost:8080'));

您的auth-status路线检查一个tokencookie,它匹配了用户身份验证令牌的预期值,然后用布尔值响应,表示用户是否已被身份验证。

完成后,您已经对后端进行了必要的更改,以便您的前端通过后端 API 跟踪用户的身份验证状态。

接下来,您将进行必要的前端更改,以实现基于 HTTP 的基于 Cookie 的代币存储。

移动到前端目录并打开Login.js文件:

1[environment second]
2cd ..
3cd front-end/src/components/
4nano Login.js

添加突出的行以修改您的登录组件中的loginUser函数:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3...
 4
 5async function loginUser(credentials) {
 6  return fetch('http://localhost:8080/login', {
 7    method: 'POST',
 8    credentials: 'include',
 9    headers: {
10      'Content-Type': 'application/json'
11    },
12    body: JSON.stringify(credentials)
13  }).then(data => data.json())
14}
15
16...

您将收集请求的凭证标题设置为包括,这告诉loginUser函数将可能被设置为 API 调用上的任何凭证发送到您刚刚在后端修改的登录路径。

接下来,您将移除setToken输入属性到Login组件,并在handleSubmit回调结束时使用该属性,因为您将不再在内存中保留代币。

您还需要在handlesubmit函数的末尾激活更新,以便您的应用在点击登录按钮时更新,新设置的tokencookie被客户端应用程序识别。

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3...
 4  const handleSubmit = async (e) => {
 5    e.preventDefault();
 6    const token = await loginUser({
 7        username: emailRef.current.value,
 8        password: passwordRef.current.value
 9    })
10    window.location.reload(false);
11  }
12...

您的Login.js文件现在应该是这样的:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/components/Login.js]
 3import React, { useRef } from 'react';
 4
 5async function loginUser(credentials) {
 6  return fetch('http://localhost:8080/login', {
 7    method: 'POST',
 8    credentials: 'include',
 9    headers: {
10      'Content-Type': 'application/json'
11    },
12    body: JSON.stringify(credentials)
13  }).then(data => data.json())
14}
15
16export default () => {
17  const emailRef = useRef();
18  const passwordRef = useRef();
19
20  const handleSubmit = async (e) => {
21    e.preventDefault();
22    const token = await loginUser({
23        username: emailRef.current.value,
24        password: passwordRef.current.value
25    })
26    window.location.reload(false);
27  }
28
29  return(
30    <div className='login-wrapper'>
31      <h1>Login</h1>
32      <form onSubmit={handleSubmit}>
33        <label>
34          <p>Username</p>
35          <input type="text" ref={emailRef} />
36        </label>
37        <label>
38          <p>Password</p>
39          <input type="password" ref={passwordRef} />
40        </label>
41        <div>
42          <button type="submit">Submit</button>
43        </div>
44      </form>
45    </div>
46  );
47}

保存并关闭文件。

由于您不再将身份验证令牌存储在内存中,因此在确定用户是否应该登录或是否可以访问私人资产时,您无法检查是否存在身份验证令牌。

要进行这些更改,请打开您的前端的App.js文件:

1[environment second]
2cd ..
3nano App.js

反应包中导入useState链接,并初始化一个新的身份验证状态变量及其设置,以反映用户的身份验证状态:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState } from 'react'
 7
 8...
 9
10function App() {
11  let [authenticated, setAuthenticated] = useState(false);
12
13  if (!token) {
14    return <Login setToken={setToken} />
15  }
16
17  return(
18    <div className="App wrapper">
19      <h1 className="App-header">
20        JWT-Storage-Tutorial Application
21      </h1>
22      <BrowserRouter>
23        <Switch>
24          <Route path="/subscriber-feed">
25            <SubscriberFeed />
26          </Route>
27        </Switch>
28      </BrowserRouter>
29    </div>
30  );
31}
32
33export default App;

useState链接将通过向您的后端API发出请求来检查用户的身份验证状态,该请求可以告诉您是否一个有效的身份验证标记被积极保留为前端客户端的cookie。

接下来,删除setTokengetToken函数、代币变量和login组件的条件渲染,然后创建两个名为getAuthStatusisAuthenticated的新函数,并使用突出的行:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState } from 'react'
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11
12function App() {
13  let [authenticated, setAuthenticated] = useState(false);
14
15  async function getAuthStatus() {
16    return fetch('http://localhost:8080/auth-status', {
17      method: 'GET',
18      credentials: 'include',
19      headers: {
20        'Content-Type': 'application/json'
21      },
22    }).then(data => data.json())
23  }
24
25  async function isAuthenticated() {
26    const authStatus = await getAuthStatus();
27    setAuthenticated(authStatus.isAuthenticated);
28  }
29
30  return(
31    <div className="App wrapper">
32      <h1 className="App-header">
33        JWT-Storage-Tutorial Application
34      </h1>
35      <BrowserRouter>
36        <Switch>
37          <Route path="/subscriber-feed">
38            <SubscriberFeed />
39          </Route>
40        </Switch>
41      </BrowserRouter>
42    </div>
43  );
44}
45
46export default App;

getAuthStatus函数将向您的后端应用程序的auth-status路径提出GET请求,以检索用户的身份验证状态,取决于用户是否用有效的auth token cookie发送了请求。

通过将凭证选项的值设置为包括,fetch会将浏览器可能为用户客户端存储的任何凭证发送为cookie。

接下来,您将导入useEffect链接,带有突出线条:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState, useEffect } from 'react'
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11
12function App() {
13  let [authenticated, setAuthenticated] = useState(false);
14
15  async function getAuthStatus() {
16    return fetch('http://localhost:8080/auth-status', {
17      method: 'GET',
18      credentials: 'include',
19      headers: {
20        'Content-Type': 'application/json'
21      },
22    }).then(data => data.json())
23  }
24
25  async function isAuthenticated() {
26    const authStatus = await getAuthStatus();
27    setAuthenticated(authStatus.isAuthenticated)
28  }
29
30  useEffect(() => {
31    isAuthenticated();
32  }, [])
33
34...

此更改将调用登录路径,以检查useEffect链接中的身份验证状态. 包括一个为useEffect链接的空的依赖数组可以帮助您避免应用程序中的内存泄露。

要在应用程序主页上有条件渲染登录组件,添加突出的行:

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3...
 4
 5function App() {
 6  let [authenticated, setAuthenticated] = useState(false);
 7  let [loading, setLoading] = useState(true)
 8
 9  async function getAuthStatus() {
10    await setLoading(true);
11    return fetch('http://localhost:8080/auth-status', {
12      method: 'GET',
13      credentials: 'include',
14      headers: {
15        'Content-Type': 'application/json'
16      },
17    }).then(data => data.json())
18  }
19
20  async function isAuthenticated() {
21    const authStatus = await getAuthStatus();
22    await setAuthenticated(authStatus.isAuthenticated);
23    await setLoading(false)
24  }
25
26  useEffect(() => {
27    isAuthenticated();
28  }, [])
29
30  return (
31    <>
32      {!loading && (
33        <>
34          {!authenticated && <Login />}
35
36          {authenticated && (
37            <div className="App wrapper">
38              <h1 className="App-header">
39                JWT-Storage-Tutorial Application
40              </h1>
41              <BrowserRouter>
42                <Switch>
43                  <Route path="/subscriber-feed">
44                    <SubscriberFeed />
45                  </Route>
46                  <Route path="/xss-helper">
47                    <XSSHelper />
48                  </Route>
49                </Switch>
50              </BrowserRouter>
51            </div>
52          )}
53        </>
54      )}
55    </>
56  );
57}
58
59export default App;

如果身份验证变量设置为,您的应用程序将返回登录组件,否则,应用程序首页和所有路径,包括私人页面,将返回。

您将添加一个新的加载状态变量,以避免渲染任何东西,直到您的后端应用程序的auth-status路径的调用完成. 由于authenticated状态变量最初设置为false,客户端将假定用户没有登录,直到 API 调用authentication-status路径完成,并更新authenticated状态变量。

接下来,您将创建一个logoutUser函数,在后端API上调用您的logout路径。

 1[environment second]
 2[label jwt-storage-tutorial/front-end/src/App.js]
 3import logo from './logo.svg';
 4import './App.css';
 5
 6import { useState, useEffect } from 'react';
 7
 8import { BrowserRouter, Route, Switch } from 'react-router-dom';
 9import SubscriberFeed from "./components/SubscriberFeed";
10import Login from './components/Login';
11import XSSHelper from './components/XSSHelper'
12
13function App() {
14  let [authenticated, setAuthenticated] = useState(false);
15  let [loading, setLoading] = useState(true)
16
17  async function getAuthStatus() {
18    await setLoading(true);
19    return fetch('http://localhost:8080/auth-status', {
20      method: 'GET',
21      credentials: 'include',
22      headers: {
23        'Content-Type': 'application/json'
24      },
25    }).then(data => data.json())
26  }
27
28  async function isAuthenticated() {
29    const authStatus = await getAuthStatus();
30    await setAuthenticated(authStatus.isAuthenticated);
31    await setLoading(false);
32  }
33
34  async function logoutUser() {
35    await fetch('http://localhost:8080/logout', {
36      method: 'POST',
37      credentials: 'include',
38      headers: {
39        'Content-Type': 'application/json'
40      },
41    })
42    isAuthenticated();
43  }
44
45  useEffect(() => {
46    isAuthenticated();
47  }, [])
48
49  return (
50    <>
51      {!loading && (
52        <>
53          {!authenticated && <Login />}
54
55          {authenticated && (
56            <div className="App wrapper">
57              <h1 className="App-header">
58                JWT-Storage-Tutorial Application
59              </h1>
60              <button onClick={logoutUser}>Logout</button>
61              <BrowserRouter>
62                <Switch>
63                  <Route path="/subscriber-feed">
64                    <SubscriberFeed />
65                  </Route>
66                  <Route path="/xss-helper">
67                    <XSSHelper />
68                  </Route>
69                </Switch>
70              </BrowserRouter>
71            </div>
72          )}
73        </>
74      )}
75    </>
76  );
77}
78
79export default App;

您将创建一个退出按钮来退出用户,将其onClick属性设置为呼叫后端API上的logout路径的回复函数。

您还将在登录回复函数的末尾呼叫isAuthenticated函数,该函数将更新您的应用程序的状态,通过将authenticated状态变量设置为false来反映用户的未经身份验证的状态。

保存并关闭文件完成后。

现在您可以测试基于 HTTP 的基于 Cookie 的代币存储系统. 更新 Web 应用程序以实现您刚刚做的更改。

然后, 清除您的浏览器存储的内容以删除浏览器存储中的任何延迟的代币。接下来,导航到同样的恶意编写的URL,如在 步骤 4中,看看攻击者是否仍然可以通过注入的JavaScript来窃取您的代币:

1localhost:3000/xss-helper?code=<a href="javascript:alert(`Your token object is ${localStorage.getItem('token')}. It has been sent to a malicious server >:)`);">Click Me!</a>

您可能需要再次登录您的网站以查看XSS Helper Active行. 您应该在点击阅读点击我!的链接后看到下面的爆炸窗口,该窗口表示您的代币对象是无效:

Screencapture of the failed XSS attack that results in <code>null</code>

注入的 JavaScript 无法找到标记对象,所以 pop-up 会显示一个null值。

您现在应该能够通过按 Logout按钮退出应用程序。

在此步骤中,您改进了您的应用程序的安全性,通过从使用浏览器存储器来使用HTTP仅存储的cookie。

结论

在本教程中,您创建了一个 React 和 Node 网页应用程序,在 Docker 容器中使用用户登录功能。 您实施了使用易受攻击的代码存储方法的身份验证系统,以测试您的网站的安全性。 您随后利用了这种方法以反射的 XSS 攻击负载来利用,从而允许您在使用浏览器存储来存储身份验证 Cookie 时评估漏洞。 最后,您在初始实施中通过设置使用 HTTP 仅存储 cookie 的身份验证系统来缓解 XSS 漏洞。

为了提高应用程序身份验证流程的安全性和可用性,您可以整合第三方身份验证工具,如 PassportJSOAuth API,如 DigitalOcean 的 OAuth API

Published At
Categories with 技术
comments powered by Disqus