在 React 上创建具有双因素身份验证的注册和登录

2025-05-25

在 React 上创建具有双因素身份验证的注册和登录

这篇文章是关于什么的?

如果您考虑过构建任何仪表板,您可能已经意识到需要实现身份验证。
您可能已经熟悉“登录”和“注册”之类的术语。您可能已经使用它们在 dev.to 上注册过 😅

如今,越来越多的公司致力于保障您的账户安全,并建议您添加双重身份验证。
双重身份验证是额外的保护措施;它要求您输入一个验证码,该验证码可以在外部服务、短信、电子邮件或身份验证应用程序中找到。

在本文中,您将学习如何使用 React、Novu 和 Node.js 构建使用双因素身份验证的应用程序。

安全

什么是双因素身份验证(2FA)?

双因素身份验证(有时也称为双重身份验证)是一种额外的安全措施,允许用户在访问帐户之前确认其身份。
它可以通过硬件令牌、短信、推送通知和生物识别技术来实现,应用程序需要这些验证才能授权用户执行各种操作。

在本文中,我们将使用 SMS 文本消息 2FA 创建一个应用程序,该应用程序接受用户的凭据并在授予用户访问权限之前验证他们的身份。

Novu——第一个开源通知基础设施

简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似于开发者社区的Websockets中的铃铛图标)、电子邮件、短信等等。

如果您能通过为该库加注星标来帮助我们,我将不胜感激🤩
https://github.com/novuhq/novu

GitHub

设置 Node.js 服务器

创建包含两个名为客户端和服务器的子文件夹的项目文件夹。

mkdir auth-system
cd auth-system
mkdir client server
Enter fullscreen mode Exit fullscreen mode

导航到server文件夹并创建一个 package.json 文件。

cd server & npm init -y
Enter fullscreen mode Exit fullscreen mode

安装 Express.js、CORS 和 Nodemon。

npm install express cors nodemon
Enter fullscreen mode Exit fullscreen mode

Express.js 是一个快速、极简的框架,它提供了多种使用 Node.js 构建 Web 应用程序的功能。CORS 是一个 Node.js ,允许不同域之间进行通信。Nodemon是一个Node.js 工具,可在检测到文件更改后自动重启服务器。

创建一个index.js文件——Web 服务器的入口点。

touch index.js
Enter fullscreen mode Exit fullscreen mode

设置一个简单的 Node.js 服务器,如下所示:

const express = require("express");
const cors = require("cors");
const app = express();
const PORT = 4000;

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

app.get("/api", (req, res) => {
    res.json({ message: "Hello world" });
});

app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

构建应用程序用户界面

在本节中,我们将构建应用程序的用户界面,允许用户注册并登录应用程序。用户可以创建帐户、登录并通过短信执行双重身份验证 (2FA),然后才有权查看仪表板。

在客户端文件夹中创建一个新的 React.js 项目。

cd client
npx create-react-app ./
Enter fullscreen mode Exit fullscreen mode

从 React 应用中删除 logo 和测试文件等冗余文件,并更新App.js文件以显示Hello World如下内容。

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

导航到src/index.css文件并复制以下代码。它包含设计此项目所需的所有 CSS。

@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    font-family: "Space Grotesk", sans-serif;
}
input {
    height: 45px;
    padding: 10px 15px;
    margin-bottom: 15px;
}
button {
    width: 200px;
    outline: none;
    border: none;
    padding: 15px;
    cursor: pointer;
    font-size: 16px;
}
.login__container,
.signup__container,
.verify,
.dashboard {
    width: 100%;
    min-height: 100vh;
    padding: 50px 70px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.login__form,
.verify__form,
.signup__form {
    width: 100%;
    display: flex;
    flex-direction: column;
}
.loginBtn,
.signupBtn,
.codeBtn {
    background-color: green;
    color: #fff;
    margin-bottom: 15px;
}
.signOutBtn {
    background-color: #c21010;
    color: #fff;
}
.link {
    cursor: pointer;
    color: rgb(39, 147, 39);
}
.code {
    width: 50%;
    text-align: center;
}
.verify__form {
    align-items: center;
}

@media screen and (max-width: 800px) {
    .login__container,
    .signup__container,
    .verify {
        padding: 30px;
    }
}
Enter fullscreen mode Exit fullscreen mode

安装 React Router - 一个 JavaScript 库,使我们能够在 React 应用程序中的页面之间导航。

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

在 React 应用程序内创建一个包含Signup.jsLogin.js和文件PhoneVerify.js的components 文件夹Dashboard.js

mkdir components
cd components
touch Signup.js Login.js PhoneVerify.js Dashboard.js
Enter fullscreen mode Exit fullscreen mode

更新App.js文件以通过 React Router 在不同的路由上呈现新创建的组件。

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Login from "./components/Login";
import Signup from "./components/Signup";
import Dashboard from "./components/Dashboard";
import PhoneVerify from "./components/PhoneVerify";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path='/' element={<Login />} />
                <Route path='/register' element={<Signup />} />
                <Route path='/dashboard' element={<Dashboard />} />
                <Route path='phone/verify' element={<PhoneVerify />} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

登录页面

将以下代码复制到Login.js文件中。它接受用户的电子邮件和密码。

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const Login = () => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ email, password });
        setPassword("");
        setEmail("");
    };

    const gotoSignUpPage = () => navigate("/register");

    return (
        <div className='login__container'>
            <h2>Login </h2>
            <form className='login__form' onSubmit={handleSubmit}>
                <label htmlFor='email'>Email</label>
                <input
                    type='text'
                    id='email'
                    name='email'
                    value={email}
                    required
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='password'>Password</label>
                <input
                    type='password'
                    name='password'
                    id='password'
                    minLength={8}
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <button className='loginBtn'>SIGN IN</button>
                <p>
                    Don't have an account?{" "}
                    <span className='link' onClick={gotoSignUpPage}>
                        Sign up
                    </span>
                </p>
            </form>
        </div>
    );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

登录

注册页面

将以下代码复制到Signup.js文件中。它接受用户的电子邮件、用户名、电话和密码。

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const Signup = () => {
    const [email, setEmail] = useState("");
    const [username, setUsername] = useState("");
    const [tel, setTel] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ email, username, tel, password });
        setEmail("");
        setTel("");
        setUsername("");
        setPassword("");
    };
    const gotoLoginPage = () => navigate("/");

    return (
        <div className='signup__container'>
            <h2>Sign up </h2>
            <form className='signup__form' onSubmit={handleSubmit}>
                <label htmlFor='email'>Email Address</label>
                <input
                    type='email'
                    name='email'
                    id='email'
                    value={email}
                    required
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='username'>Username</label>
                <input
                    type='text'
                    id='username'
                    name='username'
                    value={username}
                    required
                    onChange={(e) => setUsername(e.target.value)}
                />
                <label htmlFor='tel'>Phone Number</label>
                <input
                    type='tel'
                    name='tel'
                    id='tel'
                    value={tel}
                    required
                    onChange={(e) => setTel(e.target.value)}
                />
                <label htmlFor='tel'>Password</label>
                <input
                    type='password'
                    name='password'
                    id='password'
                    minLength={8}
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <button className='signupBtn'>SIGN UP</button>
                <p>
                    Already have an account?{" "}
                    <span className='link' onClick={gotoLoginPage}>
                        Login
                    </span>
                </p>
            </form>
        </div>
    );
};

export default Signup;
Enter fullscreen mode Exit fullscreen mode

注册

PhoneVerify 页面

更新PhoneVerify.js文件以包含以下代码。它接受发送到用户电话号码的验证码。

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const PhoneVerify = () => {
    const [code, setCode] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ code });
        setCode("");
        navigate("/dashboard");
    };
    return (
        <div className='verify'>
            <h2 style={{ marginBottom: "30px" }}>Verify your Phone number</h2>
            <form className='verify__form' onSubmit={handleSubmit}>
                <label htmlFor='code' style={{ marginBottom: "10px" }}>
                    A code has been sent your phone
                </label>
                <input
                    type='text'
                    name='code'
                    id='code'
                    className='code'
                    value={code}
                    onChange={(e) => setCode(e.target.value)}
                    required
                />
                <button className='codeBtn'>AUTHENTICATE</button>
            </form>
        </div>
    );
};

export default PhoneVerify;
Enter fullscreen mode Exit fullscreen mode

仪表板

仪表板页面

将以下代码复制到Dashboard.js文件中。这是一个受保护的页面,只有经过身份验证的用户才能访问。

import React, {useState} from "react";
import { useNavigate } from "react-router-dom";

const Dashboard = () => {
    const navigate = useNavigate();

useEffect(() => {
        const checkUser = () => {
            if (!localStorage.getItem("username")) {
                navigate("/");
            }
        };
        checkUser();
    }, [navigate]);

    const handleSignOut = () => {
        localStorage.removeItem("username");
        navigate("/");
    };

    return (
        <div className='dashboard'>
            <h2 style={{ marginBottom: "30px" }}>Howdy, David</h2>
            <button className='signOutBtn' onClick={handleSignOut}>
                SIGN OUT
            </button>
        </div>
    );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

创建身份验证工作流程

在这里,我们将为应用程序创建身份验证工作流程。
创建帐户时,应用程序会接受用户的电子邮件、用户名、电话号码和密码。然后将用户重定向到登录页面,该页面需要输入电子邮件和密码。应用程序会向用户的电话号码发送验证码,以验证其身份,之后用户才能查看仪表板页面。

注册路线

在组件内创建一个函数Signup,将用户的凭据发送到 Node.js 服务器。

const postSignUpDetails = () => {
    fetch("http://localhost:4000/api/register", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
            tel,
            username,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            console.log(data);
        })
        .catch((err) => console.error(err));
};

const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Call it within the submit function
    postSignUpDetails();
    setEmail("");
    setTel("");
    setUsername("");
    setPassword("");
};
Enter fullscreen mode Exit fullscreen mode

index.js在服务器上的文件中创建一个接受用户凭据的POST 路由。

app.post("/api/register", (req, res) => {
    const { email, password, tel, username } = req.body;
    //👇🏻 Logs the credentials to the console
    console.log({ email, password, tel, username });
})
Enter fullscreen mode Exit fullscreen mode

由于我们需要保存用户的凭据,因此请按如下方式更新 POST 路由:

//👇🏻 An array containing all the users
const users = [];

//👇🏻 Generates a random string as the ID
const generateID = () => Math.random().toString(36).substring(2, 10);

app.post("/api/register", (req, res) => {
    //👇🏻 Get the user's credentials
    const { email, password, tel, username } = req.body;

    //👇🏻 Checks if there is an existing user with the same email or password
    let result = users.filter((user) => user.email === email || user.tel === tel);

    //👇🏻 if none
    if (result.length === 0) {
        //👇🏻 creates the structure for the user
        const newUser = { id: generateID(), email, password, username, tel };
        //👇🏻 Adds the user to the array of users
        users.push(newUser);
        //👇🏻 Returns a message
        return res.json({
            message: "Account created successfully!",
        });
    }
    //👇🏻 Runs if a user exists
    res.json({
        error_message: "User already exists",
    });
});
Enter fullscreen mode Exit fullscreen mode

更新组件postSignUpDetails内的功能Signup,通知用户已注册成功。

const postSignUpDetails = () => {
    fetch("http://localhost:4000/api/register", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
            tel,
            username,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                alert(data.message);
                navigate("/");
            }
        })
        .catch((err) => console.error(err));
};
Enter fullscreen mode Exit fullscreen mode

上面的代码片段在导航到登录路由之前检查服务器返回的数据是否包含错误消息。如果有错误,则显示错误消息。

登录路线

使用登录组件创建一个函数,将用户的电子邮件和密码发送到服务器。

const postLoginDetails = () => {
    fetch("http://localhost:4000/api/login", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            console.log(data);
        })
        .catch((err) => console.error(err));
};
const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Calls the function
    postLoginDetails();
    setPassword("");
    setEmail("");
};
Enter fullscreen mode Exit fullscreen mode

在服务器上创建用于验证用户的 POST 路由。

app.post("/api/login", (req, res) => {
    //👇🏻 Accepts the user's credentials
    const { email, password } = req.body;
    //👇🏻 Checks for user(s) with the same email and password
    let result = users.filter(
        (user) => user.email === email && user.password === password
    );

    //👇🏻 If no user exists, it returns an error message
    if (result.length !== 1) {
        return res.json({
            error_message: "Incorrect credentials",
        });
    }
    //👇🏻 Returns the username of the user after a successful login
    res.json({
        message: "Login successfully",
        data: {
            username: result[0].username,
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

更新postLoginDetails以显示来自服务器的响应。

const postLoginDetails = () => {
    fetch("http://localhost:4000/api/login", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                //👇🏻 Logs the username to the console
                console.log(data.data);
                //👇🏻 save the username to the local storage
                localStorage.setItem("username", data.data.username);
                //👇🏻 Navigates to the 2FA route
                navigate("/phone/verify");
            }
        })
        .catch((err) => console.error(err));
};
Enter fullscreen mode Exit fullscreen mode

如果出现错误,上面的代码片段会向用户显示错误消息;否则,它会将从服务器获取的用户名保存到本地存储中,以便于识别。

在接下来的部分中,我将指导您使用 Novu 添加 SMS 双因素身份验证。

如何将 Novu 添加到 Node.js 应用程序

Novu 允许我们向您的软件应用程序添加各种形式的通知,例如短信、电子邮件、聊天和推送消息。

要安装 Novu Node.js SDK,请在您的服务器上运行以下代码片段。

npm install @novu/node
Enter fullscreen mode Exit fullscreen mode

运行以下代码创建一个 Novu 项目。您可以使用个性化仪表板。

npx novu init
Enter fullscreen mode Exit fullscreen mode

在创建 Novu 项目之前,您需要使用 Github 登录。以下代码片段包含运行后应遵循的步骤npx novu init

Now let's setup your account and send your first notification
❓ What is your application name? Devto Clone
❓ Now lets setup your environment. How would you like to proceed?
   > Create a free cloud account (Recommended)
❓ Create your account with:
   > Sign-in with GitHub
❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
    > Yes
✔️ Create your account successfully.

We've created a demo web page for you to see novu notifications in action.
Visit: http://localhost:57807/demo to continue
Enter fullscreen mode Exit fullscreen mode

访问演示网页http://localhost:57807/demo,复制您的订阅者 ID,然后点击“跳过教程”按钮。我们将在本教程的后续部分使用它。

Novu1

复制Novu 管理平台API 密钥下设置部分中提供的 API 密钥 

Novu2

从包中导入 Novu 并使用服务器上的 API 密钥创建实例。

//👇🏻 Within server/index.js

const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");
Enter fullscreen mode Exit fullscreen mode

使用 Novu 添加短信双因素身份验证

Novu 支持多种短信工具,例如 Twilio、Nexmo、Plivo、Amazon SNS 等。在本节中,您将学习如何将 Twilio 短信添加到 Novu。

设置 Twilio 帐户

前往 Twilio 主页 并创建帐户。您需要验证您的电子邮件和电话号码。

 登录后前往您的 Twilio 控制台。

在仪表板上生成一个 Twilio 电话号码。此电话号码是一个虚拟号码,可让您通过 Twilio 进行通信。

将 Twilio 电话号码复制到计算机上的某个位置;以便在本教程的后面使用。

向下滚动到“帐户信息”部分,然后将帐户 SID 和授权令牌复制并粘贴到计算机上的某个位置。(本教程后面会用到)。

将 Twilio SMS 连接到 Novu

从 Novu 仪表板的侧边栏中选择 Integrations Store,然后向下滚动到 SMS 部分。

选择 Twilio 并输入 Twilio 提供的所需凭据,然后单击“连接”。

Novu2

接下来,通过选择侧边栏上的通知来创建通知模板。

Novu4

从侧窗格中选择工作流编辑器并创建如下所示的工作流:

Novu5

单击工作流程中的短信,并将以下文本添加到消息内容字段。

Your verification code is {{code}}
Enter fullscreen mode Exit fullscreen mode

Novu 允许您使用Handlebars 模板引擎向模板添加动态内容或数据 。代码变量的数据将作为请求的有效负载插入到模板中。

返回index.js服务器上的文件,并创建一个函数,用于在用户登录应用程序时发送短信来验证用户。将以下代码添加到index.js文件中:

//👇🏻 Generates the code to be sent via SMS
const generateCode = () => Math.random().toString(36).substring(2, 12);

const sendNovuNotification = async (recipient, verificationCode) => {
    try {
        let response = await novu.trigger("<NOTIFICATION_TEMPLATE_ID>", {
            to: {
                subscriberId: recipient,
                phone: recipient,
            },
            payload: {
                code: verificationCode,
            },
        });
        console.log(response);
    } catch (err) {
        console.error(err);
    }
};
Enter fullscreen mode Exit fullscreen mode

上面的代码片段接受收件人的电话号码和验证码作为参数。

更新loginPOST 路由,以便在用户登录应用程序后通过 Novu 发送短信。

//👇🏻 variable that holds the generated code
let code;

app.post("/api/login", (req, res) => {
    const { email, password } = req.body;

    let result = users.filter(
        (user) => user.email === email && user.password === password
    );

    if (result.length !== 1) {
        return res.json({
            error_message: "Incorrect credentials",
        });
    }
    code = generateCode();

    //👇🏻 Send the SMS via Novu
    sendNovuNotification(result[0].tel, code);

    res.json({
        message: "Login successfully",
        data: {
            username: result[0].username,
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

为了验证用户输入的代码,请更新PhoneVerify组件以将代码发送到服务器。

const postVerification = async () => {
    fetch("http://localhost:4000/api/verification", {
        method: "POST",
        body: JSON.stringify({
            code,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                //👇🏻 Navigates to the dashboard page
                navigate("/dashboard");
            }
        })
        .catch((err) => console.error(err));
};
const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Calls the function
    postVerification();
    setCode("");
};
Enter fullscreen mode Exit fullscreen mode

在服务器上创建一个 POST 路由,接受代码并检查它是否与后端的代码相同。

app.post("/api/verification", (req, res) => {
    if (code === req.body.code) {
        return res.json({ message: "You're verified successfully" });
    }
    res.json({
        error_message: "Incorrect credentials",
    });
});
Enter fullscreen mode Exit fullscreen mode

恭喜!🎊 您已完成本教程的项目。

结论

到目前为止,您已经了解了双因素身份验证是什么、它的各种形式以及如何将其添加到 Web 应用程序中。

软件应用程序添加了双重身份验证,以保护用户凭据和用户可访问的资源。根据您的应用程序,您可以在发生重大更改的特定部分添加双重身份验证。

该应用程序的源代码可以在这里找到:https://github.com/novuhq/blog/tree/main/2FA-with-react-nodejs-and-novu

感谢您的阅读!

PS:如果您能通过 star 来帮助我们,我将非常感激 🤩
https://github.com/novuhq/novu

GitHub

您还可以了解如何获得 GitHub 星标

文章来源:https://dev.to/novu/creating-a-registration-and-a-login-with-two-factor-authentication-on-react-2p2i
PREV
使用 React、NodeJS 和 AI 创建简历生成器 🚀 TL;DR
NEXT
使用 Puppeteer 和 React 构建交互式屏幕共享应用程序🤯