教程:使用本机身份验证 JavaScript SDK 将用户注册到 React 单页应用(预览版)

适用于绿色圆圈,带有白色复选标记符号,指示以下内容适用于外部租户。 外部租户(了解详细信息

本教程介绍如何构建 React 单页应用,用于通过原生认证 JavaScript SDK 注册用户。

在本教程中,你将:

  • 创建 React Next.js 项目。
  • 将 MSAL JS SDK 添加到其中。
  • 添加应用的 UI 组件。
  • 设置项目以注册用户。

先决条件

创建 React 项目并安装依赖项

在计算机中选择的位置,运行以下命令以创建名为 reactspa的新 React 项目,导航到项目文件夹,然后安装包:

npx create-next-app@latest
cd reactspa
npm install

成功运行命令后,应具有具有以下结构的应用:

spasample/
└──node_modules/
   └──...
└──public/
   └──...
└──src/
   └──app/
      └──favicon.ico
      └──globals.css
      └──page.tsx
      └──layout.tsx
└──postcss.config.mjs
└──package-lock.json
└──package.json
└──tsconfig.json
└──README.md
└──next-env.d.ts
└──next.config.ts

将 JavaScript SDK 添加到项目

若要在应用中使用本机身份验证 JavaScript SDK,请使用终端通过以下命令安装它:

npm install @azure/msal-browser

本机身份验证功能是库的 azure-msal-browser 一部分。 若要使用本机身份验证功能,请从@azure/msal-browser/custom-auth中导入。 例如:

  import CustomAuthPublicClientApplication from "@azure/msal-browser/custom-auth";

添加客户端配置

在本部分中,将定义本机身份验证公共客户端应用程序的配置,使它能够与 SDK 的接口进行交互。 为此,请创建名为 src/config/auth-config.ts 的文件,然后添加以下代码:

export const customAuthConfig: CustomAuthConfiguration = {
  customAuth: {
    challengeTypes: ["password", "oob", "redirect"],
    authApiProxyUrl: "http://localhost:3001/api",
  },
  auth: {
    clientId: "Enter_the_Application_Id_Here",
    authority: "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com",
    redirectUri: "/",
    postLogoutRedirectUri: "/",
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: "sessionStorage",
  },
  system: {
    loggerOptions: {
      loggerCallback: (
        level: LogLevel,
        message: string,
        containsPii: boolean
      ) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
    },
  },
};

在代码中,找到占位符:

  • Enter_the_Application_Id_Here 然后将其替换为之前注册的应用的应用程序(客户端)ID。

  • Enter_the_Tenant_Subdomain_Here 然后将其替换为Microsoft Entra 管理中心中的租户子域。 例如,如果租户主域名是 contoso.onmicrosoft.com,请使用 contoso。 如果没有租户名称,请了解如何 阅读租户详细信息

创建 UI 组件

此应用从用户收集用户详细信息,例如给定的名称、用户名(电子邮件)、密码和一次性密码。 因此,应用需要有一个表单来收集此信息。

  1. src 文件夹中创建名为 src/app/sign-up 的文件夹。

  2. 创建 注册/组件/InitialForm.tsx 文件,然后粘贴 注册/components/InitialForm.tsx 中的代码。 此组件显示一个用于收集用户注册信息的表单。

  3. 创建 注册/组件/CodeForm.tsx 文件,然后粘贴 注册/组件/CodeForm.tsx 中的代码。 此组件显示一个表单,用于收集发送给用户的一次性密码。 对于具有密码的电子邮件或采用一次性密码身份验证方法的电子邮件,需要此表单。

  4. 如果选择的身份验证方法是 使用密码发送电子邮件,请创建 注册/组件/PasswordForm.tsx 文件,然后粘贴 注册/组件/PasswordForm.tsx 中的代码。 此组件显示密码输入窗体。

处理表单交互

在本部分中,你将添加代码来处理注册表单的各种交互,例如提交用户注册详情、一次性验证码或密码。

创建 注册/page.tsx 来处理注册流的逻辑。 在此文件中:

  • 导入必要的组件,并根据状态显示正确的表单。 请参阅 注册/page.tsx 中的完整示例:

        import { useEffect, useState } from "react";
        import { customAuthConfig } from "../../config/auth-config";
        import { styles } from "./styles/styles";
        import { InitialFormWithPassword } from "./components/InitialFormWithPassword";
    
        import {
        CustomAuthPublicClientApplication,
        ICustomAuthPublicClientApplication,
        SignUpCodeRequiredState,
        // Uncomment if your choice of authentication method is email with password
        // SignUpPasswordRequiredState,
        SignUpCompletedState,
        AuthFlowStateBase,
      } from "@azure/msal-browser/custom-auth";
    
        import { SignUpResultPage } from "./components/SignUpResult";
        import { CodeForm } from "./components/CodeForm";
        import { PasswordForm } from "./components/PasswordForm";    
    export default function SignUpPassword() {
        const [authClient, setAuthClient] = useState<ICustomAuthPublicClientApplication | null>(null);
        const [firstName, setFirstName] = useState("");
        const [lastName, setLastName] = useState("");
        const [jobTitle, setJobTitle] = useState("");
        const [city, setCity] = useState("");
        const [country, setCountry] = useState("");
        const [email, setEmail] = useState("");
        //Uncomment if your choice of authentication method is email with password
        //const [password, setPassword] = useState("");
        const [code, setCode] = useState("");
        const [error, setError] = useState("");
        const [loading, setLoading] = useState(false);
        const [signUpState, setSignUpState] = useState<AuthFlowStateBase | null>(null);
        const [loadingAccountStatus, setLoadingAccountStatus] = useState(true);
        const [isSignedIn, setSignInState] = useState(false);
    
        useEffect(() => {
            const initializeApp = async () => {
                const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
                setAuthClient(appInstance);
            };
            initializeApp();
        }, []);
    
        useEffect(() => {
            const checkAccount = async () => {
                if (!authClient) return;
                const accountResult = authClient.getCurrentAccount();
                if (accountResult.isCompleted()) {
                    setSignInState(true);
                }
                setLoadingAccountStatus(false);
            };
            checkAccount();
        }, [authClient]);
    
        const renderForm = () => {
            if (loadingAccountStatus) {
                return;
            }
            if (isSignedIn) {
                return (
                    <div style={styles.signed_in_msg}>Please sign out before processing the sign up.</div>
                );
            }
            if (signUpState instanceof SignUpCodeRequiredState) {
                return (
                    <CodeForm
                        onSubmit={handleCodeSubmit}
                        code={code}
                        setCode={setCode}
                        loading={loading}
                    />
                );
            } 
            //Uncomment the following block of code if your choice of authentication method is email with password 
            /*
            else if(signUpState instanceof SignUpPasswordRequiredState) {
                return <PasswordForm
                    onSubmit={handlePasswordSubmit}
                    password={password}
                    setPassword={setPassword}
                    loading={loading}
                />;
            }
            */
            else if (signUpState instanceof SignUpCompletedState) {
                return <SignUpResultPage />;
            } else {
                return (
                    <InitialForm
                        onSubmit={handleInitialSubmit}
                        firstName={firstName}
                        setFirstName={setFirstName}
                        lastName={lastName}
                        setLastName={setLastName}
                        jobTitle={jobTitle}
                        setJobTitle={setJobTitle}
                        city={city}
                        setCity={setCity}
                        country={country}
                        setCountry={setCountry}
                        email={email}
                        setEmail={setEmail}
                        loading={loading}
                    />
                );
            }
        }
        return (
            <div style={styles.container}>
                <h2 style={styles.h2}>Sign Up</h2>
                {renderForm()}
                {error && <div style={styles.error}>{error}</div>}
            </div>
        );
    }
    

    此代码还使用 客户端配置创建本机身份验证公共客户端应用的实例:

    const appInstance = await CustomAuthPublicClientApplication.create(customAuthConfig);
    setAuthClient(appInstance);
    
  • 若要处理初始表单提交,请使用以下代码片段。 请参阅 注册/page.tsx 的完整示例,了解如何将代码片段放置在何处:

    const handleInitialSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (!authClient) return;
    
        const attributes: UserAccountAttributes = {
            displayName: `${firstName} ${lastName}`,
            givenName: firstName,
            surname: lastName,
            jobTitle: jobTitle,
            city: city,
            country: country,
        };
    
        const result = await authClient.signUp({
            username: email,
            attributes
        });
        const state = result.state;
    
        if (result.isFailed()) {
            if (result.error?.isUserAlreadyExists()) {
                setError("An account with this email already exists");
            } else if (result.error?.isInvalidUsername()) {
                setError("Invalid uername");
            } else if (result.error?.isInvalidPassword()) {
                setError("Invalid password");
            } else if (result.error?.isAttributesValidationFailed()) {
                setError("Invalid attributes");
            } else if (result.error?.isMissingRequiredAttributes()) {
                setError("Missing required attributes");
            } else {
                setError(result.error?.errorData.errorDescription || "An error occurred while signing up");
            }
        } else {
            setSignUpState(state);
        }
        setLoading(false);
    };
    

    SDK 的实例方法启动 signUp() 注册流。

  • 若要处理一次性密码提交,请使用以下代码片段。 请参阅 注册/page.tsx 的完整示例,了解如何将代码片段放置在何处:

    const handleCodeSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        try {
            if (signUpState instanceof SignUpCodeRequiredState) {
                const result = await signUpState.submitCode(code);
                if (result.error) {
                    if (result.error.isInvalidCode()) {
                        setError("Invalid verification code");
                    } else {
                        setError("An error occurred while verifying the code");
                    }
                    return;
                }
                if (result.state instanceof SignUpCompletedState) {
                    setSignUpState(result.state);
                }
            }
        } catch (err) {
            setError("An unexpected error occurred");
            console.error(err);
        } finally {
            setLoading(false);
        }
    };
    
  • 若要处理密码提交,请使用以下代码片段。 如果你选择的身份验证方法是 包含密码的电子邮件,则处理密码提交。 请参阅 注册/page.tsx 的完整示例,了解如何将代码片段放置在何处:

        const handlePasswordSubmit = async (e: React.FormEvent) => {
            e.preventDefault();
            setError("");
            setLoading(true);
    
            if (signUpState instanceof SignUpPasswordRequiredState) {
                const result = await signUpState.submitPassword(password);
                const state = result.state;
    
                if (result.isFailed()) {
                    if (result.error?.isInvalidPassword()) {
                        setError("Invalid password");
                    } else {
                        setError(result.error?.errorData.errorDescription || "An error occurred while submitting the password");
                    }
                } else {
                    setSignUpState(state);
                }
            }
    
            setLoading(false);
        };
    
  • 使用 signUpState instanceof SignUpCompletedState 指示用户已注册且流程已完成。 请参阅 注册/page.tsx 的完整示例:

    if (signUpState instanceof SignUpCompletedState) {
        return <SignUpResultPage/>;
    }
    

处理注册错误

在注册期间,并非所有操作都能成功。 例如,用户可能会尝试使用已使用的电子邮件地址进行注册或提交无效的电子邮件一次性密码。 请确保在以下情况下正确处理错误:

  • signUp() 方法中启动注册流。

  • submitCode() 方法中提交一次性密码。

  • submitPassword() 方法中提交密码。 如果你选择通过电子邮件和密码注册,则需要处理此错误。

方法产生的 signUp() 错误之一是 result.error?.isRedirectRequired()。 当本机身份验证不足以完成身份验证流时,会出现这种情况。 例如,如果授权服务器需要客户端无法提供的功能。 详细了解 本机身份验证 Web 回退 以及如何在 React 应用中 支持 Web 回退

可选:注册后自动登录用户

用户成功注册后,无需启动新的登录流即可将其直接登录到应用。 为此,请使用以下代码片段。 请参阅 注册/page.tsx 的完整示例:

if (signUpState instanceof SignUpCompletedState) {
    const result = await signUpState.signIn();
    const state = result.state;
    if (result.isFailed()) {
        setError(result.error?.errorData?.errorDescription || "An error occurred during auto sign-in");
    }
    
    if (result.isCompleted()) {
        setData(result.data);
        setSignUpState(state);
    }
}

运行并测试应用

  1. 打开终端窗口并导航到应用的根文件夹:

    cd reactspa
    
  2. 若要启动 CORS 代理服务器,请在终端中运行以下命令:

    npm run cors
    
  3. 若要启动 React 应用,请打开另一个终端窗口,然后运行以下命令:

    cd reactspa
    npm start
    
  4. 打开 Web 浏览器并导航到 http://localhost:3000/sign-up。 此时会显示注册表单。

  5. 若要注册帐户,请输入详细信息,选择“ 继续 ”按钮,然后按照提示作。

接下来,可以更新 React 应用以登录用户或重置用户的密码。

在 next.config.js 中将 poweredByHeader 设置为 false

默认情况下, x-powered-by 标头包含在 HTTP 响应中,以指示应用程序由 Next.js提供支持。 但是,出于安全或自定义原因,可能需要删除或修改此标头:

const nextConfig: NextConfig = {
  poweredByHeader: false,
  /* other config options here */
};

后续步骤