适用于: 员工租户
外部租户(了解详细信息)
本教程介绍如何构建使用本机身份验证注册用户的 React 单页应用。
在本教程中,你将:
- 创建 React 项目。
- 添加应用的 UI 组件。
- 设置项目以使用用户名(电子邮件)和密码注册用户。
先决条件
- 完成 快速入门:使用本机身份验证 API 在示例 React 单页应用程序中登录用户中的步骤。 本快速入门介绍如何准备外部租户并运行 React 代码示例。
- Visual Studio Code 或其他代码编辑器。
- Node.js。
创建 React 项目并安装依赖项
在计算机中选择的位置,运行以下命令以创建名为 reactspa的新 React 项目,导航到项目文件夹,然后安装包:
npm config set legacy-peer-deps true
npx create-react-app reactspa --template typescript
cd reactspa
npm install ajv
npm installreact-router-dom
npm install
为应用添加配置文件
创建名为 src/config.js的文件,然后添加以下代码:
// App Id obatained from the Microsoft Entra portal
export const CLIENT_ID = "Enter_the_Application_Id_Here";
// URL of the CORS proxy server
const BASE_API_URL = `http://localhost:3001/api`;
// Endpoints URLs for Native Auth APIs
export const ENV = {
urlSignupStart: `${BASE_API_URL}/signup/v1.0/start`,
urlSignupChallenge: `${BASE_API_URL}/signup/v1.0/challenge`,
urlSignupContinue: `${BASE_API_URL}/signup/v1.0/continue`,
}
找到
Enter_the_Application_Id_Here
值并将其替换为在 Microsoft Entra 管理中心中注册的应用的应用程序 ID (clientId)。BASE_API_URL
指向 跨域资源共享(CORS) 代理服务器,我们稍后在本教程系列中设置该服务器。 本机身份验证 API 不支持 CORS,因此我们在 React SPA 和本机身份验证 API 之间设置 CORS 代理服务器来管理 CORS 标头。
设置 React 应用以调用本机身份验证 API 并处理响应
若要使用本机身份验证 API 完成身份验证流(例如注册流),应用会发出调用并处理响应。 例如,应用启动注册流并等待响应,然后提交用户属性并再次等待,直到用户成功注册。
设置对原生身份验证 API 的客户端调用
在本部分中,将定义如何调用本机身份验证并处理响应:
在 src中创建名为 客户端 的文件夹。
创建名为 scr/client/RequestClient.ts的文件,然后添加以下代码片段:
import { ErrorResponseType } from "./ResponseTypes"; export const postRequest = async (url: string, payloadExt: any) => { const body = new URLSearchParams(payloadExt as any); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body, }); if (!response.ok) { try { const errorData: ErrorResponseType = await response.json(); throw errorData; } catch (jsonError) { const errorData = { error: response.status, description: response.statusText, codes: [], timestamp: "", trace_id: "", correlation_id: "", }; throw errorData; } } return await response.json(); };
此代码定义应用如何调用本机身份验证 API 并处理响应。 每当应用需要启动身份验证流时,它就会通过指定 URL 和有效负载数据来使用
postRequest
函数。
定义应用对本机身份验证 API 发出的调用类型
在注册流期间,应用对本机身份验证 API 进行多次调用。
若要定义这些调用,请创建名为 scr/client/RequestTypes.ts的文件,然后添加以下代码片段:
//SignUp
export interface SignUpStartRequest {
client_id: string;
username: string;
challenge_type: string;
password?: string;
attributes?: Object;
}
export interface SignUpChallengeRequest {
client_id: string;
continuation_token: string;
challenge_type?: string;
}
export interface SignUpFormPassword {
name: string;
surname: string;
username: string;
password: string;
}
//OTP
export interface ChallengeForm {
continuation_token: string;
oob?: string;
password?: string;
}
定义从本机身份验证 API 接收的响应应用的类型
若要定义应用可从本机身份验证 API 接收的用于注册作的响应类型,请创建名为 src/client/ResponseTypes.ts的文件,然后添加以下代码片段:
export interface SuccessResponseType {
continuation_token?: string;
challenge_type?: string;
}
export interface ErrorResponseType {
error: string;
error_description: string;
error_codes: number[];
timestamp: string;
trace_id: string;
correlation_id: string;
}
export interface ChallengeResponse {
binding_method: string;
challenge_channel: string;
challenge_target_label: string;
challenge_type: string;
code_length: number;
continuation_token: string;
interval: number;
}
处理注册请求
在本部分中,将添加用于处理注册流请求的代码。 这些请求的示例包括启动注册流、选择身份验证方法并提交一次性密码。
为此,请创建名为 src/client/SignUpService.ts的文件,然后添加以下代码片段:
import { CLIENT_ID, ENV } from "../config";
import { postRequest } from "./RequestClient";
import { ChallengeForm, SignUpChallengeRequest, SignUpFormPassword, SignUpStartRequest } from "./RequestTypes";
import { ChallengeResponse } from "./ResponseTypes";
//handle start a sign-up flow
export const signupStart = async (payload: SignUpFormPassword) => {
const payloadExt: SignUpStartRequest = {
attributes: JSON.stringify({
given_name: payload.name,
surname: payload.surname,
}),
username: payload.username,
password: payload.password,
client_id: CLIENT_ID,
challenge_type: "password oob redirect",
};
return await postRequest(ENV.urlSignupStart, payloadExt);
};
//handle selecting an authentication method
export const signupChallenge = async (payload: ChallengeForm):Promise<ChallengeResponse> => {
const payloadExt: SignUpChallengeRequest = {
client_id: CLIENT_ID,
challenge_type: "password oob redirect",
continuation_token: payload.continuation_token,
};
return await postRequest(ENV.urlSignupChallenge, payloadExt);
};
//handle submit one-time passcode
export const signUpSubmitOTP = async (payload: ChallengeForm) => {
const payloadExt = {
client_id: CLIENT_ID,
continuation_token: payload.continuation_token,
oob: payload.oob,
grant_type: "oob",
};
return await postRequest(ENV.urlSignupContinue, payloadExt);
};
challenge_type
属性显示客户端应用支持的身份验证方法。 此应用使用电子邮件加密码进行登录,因此挑战类型值为“密码 oob 重定向”。 详细了解挑战类型。
创建 UI 组件
此应用收集用户详细信息,例如给定的名称、姓氏(电子邮件)和密码以及用户的一次性密码。 因此,应用需要注册和一次性密码收集表单。
在 src 文件夹中创建名为 /pages/SignUp 的文件夹。
若要创建、显示和提交注册表单,请创建 src/pages/SignUp/SignUp.tsx的文件,然后添加以下代码:
import React, { useState } from 'react'; import { signupChallenge, signupStart } from '../../client/SignUpService'; import { useNavigate } from 'react-router-dom'; import { ErrorResponseType } from "../../client/ResponseTypes"; export const SignUp: React.FC = () => { const [name, setName] = useState<string>(''); const [surname, setSurname] = useState<string>(''); const [email, setEmail] = useState<string>(''); const [error, setError] = useState<string>(''); const [isLoading, setIsloading] = useState<boolean>(false); const navigate = useNavigate(); const validateEmail = (email: string): boolean => { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(String(email).toLowerCase()); }; const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!name || !surname || !email) { setError('All fields are required'); return; } if (!validateEmail(email)) { setError('Invalid email format'); return; } setError(''); try { setIsloading(true); const res1 = await signupStart({ name, surname, username: email, password }); const res2 = await signupChallenge({ continuation_token: res1.continuation_token }); navigate('/signup/challenge', { state: { ...res2} }); } catch (err) { setError("An error occurred during sign up " + (err as ErrorResponseType).error_description); } finally { setIsloading(false); } }; return ( <div className="sign-up-form"> <form onSubmit={handleSubmit}> <h2>Sign Up</h2> <div className="form-group"> <label>Name:</label> <input type="text" value={name} onChange={(e) => setName(e.target.value)} required /> </div> <div className="form-group"> <label>Last Name:</label> <input type="text" value={surname} onChange={(e) => setSurname(e.target.value)} required /> </div> <div className="form-group"> <label>Email:</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required /> </div> {error && <div className="error">{error}</div>} {isLoading && <div className="warning">Sending request...</div>} <button type="submit">Sign Up</button> </form> </div> ); };
若要创建、显示和提交一次性密码表单,请创建 src/pages/signup/SignUpChallenge.tsx的文件,然后添加以下代码:
import React, { useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { signUpSubmitOTP } from "../../client/SignUpService"; import { ErrorResponseType } from "../../client/ResponseTypes"; export const SignUpChallenge: React.FC = () => { const { state } = useLocation(); const navigate = useNavigate(); const { challenge_target_label, challenge_type, continuation_token, code_length } = state; const [code, setCode] = useState<string>(""); const [error, setError] = useState<string>(""); const [isLoading, setIsloading] = useState<boolean>(false); const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!code) { setError("All fields are required"); return; } setError(""); try { setIsloading(true); const res = await signUpSubmitOTP({ continuation_token, oob: code }); navigate("/signup/completed"); } catch (err) { setError("An error occurred during sign up " + (err as ErrorResponseType).error_description); } finally { setIsloading(false); } }; return ( <div className="sign-up-form"> <form onSubmit={handleSubmit}> <h2>Insert your one time code received at {challenge_target_label}</h2> <div className="form-group"> <label>Code:</label> <input maxLength={code_length} type="text" value={code} onChange={(e) => setCode(e.target.value)} required /> </div> {error && <div className="error">{error}</div>} {isLoading && <div className="warning">Sending request...</div>} <button type="submit">Sign Up</button> </form> </div> ); };
src/pages/signup/SignUpCompleted.tsx创建文件,然后添加以下代码:
import React from 'react'; import { Link } from 'react-router-dom'; export const SignUpCompleted: React.FC = () => { return ( <div className="sign-up-completed"> <h2>Sign Up Completed</h2> <p>Your sign-up process is complete. You can now log in.</p> <Link to="/signin" className="login-link">Go to Login</Link> </div> ); };
此页面显示成功消息和一个按钮,用于在用户成功注册后将用户带到登录页。
打开 src/App.tsx 文件,然后将其内容替换为以下代码:
import React from "react"; import { BrowserRouter, Link } from "react-router-dom"; import "./App.css"; import { AppRoutes } from "./AppRoutes"; function App() { return ( <div className="App"> <BrowserRouter> <header> <nav> <ul> <li> <Link to="/signup">Sign Up</Link> </li> <li> <Link to="/signin">Sign In</Link> </li> <li> <Link to="/reset">Reset Password</Link> </li> </ul> </nav> </header> <AppRoutes /> </BrowserRouter> </div> ); } export default App;
若要正确显示 React 应用,请执行以下作:
打开 src/App.css 文件,然后在
App-header
类中添加以下属性:min-height: 100vh;
打开 src/Index.css 文件,然后将其内容替换为 src/index.css 中的代码
添加应用路由
创建名为 src/AppRoutes.tsx的文件,然后添加以下代码:
import { Route, Routes } from "react-router-dom";
import { SignUp } from "./pages/SignUp/SignUp";
import { SignUpChallenge } from "./pages/SignUp/SignUpChallenge";
import { SignUpCompleted } from "./pages/SignUp/SignUpCompleted";
export const AppRoutes = () => {
return (
<Routes>
<Route path="/" element={<SignUp />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/signup/challenge" element={<SignUpChallenge />} />
<Route path="/signup/completed" element={<SignUpCompleted />} />
</Routes>
);
};
此时,React 应用可以将注册请求发送到本机身份验证 API,但我们需要设置 CORS 代理服务器来管理 CORS 标头。