教程:使用本机身份验证 JavaScript SDK 将用户登录到 React 单页应用

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

本教程介绍如何使用本机身份验证 JavaScript SDK 将用户登录到 React 单页应用(SPA)。

在本教程中,你将:

  • 更新 React 应用以登录用户。
  • 测试登录流。

先决条件

创建 UI 组件

在本部分中,将创建收集用户登录信息的表单:

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

  2. 创建 登录/组件/InitialForm.tsx 文件,然后粘贴 登录/components/InitialForm.tsx 中的代码。 此组件显示一个窗体,用于收集用户的用户名(即电子邮件)。

  3. 如果选择的身份验证方法是电子邮件和一次性密码,请创建 登录/组件/CodeForm.tsx 文件,然后粘贴 登录/组件/CodeForm.tsx 中的代码。 如果管理员在 Microsoft Entra 管理中心将电子邮件一次性密码设置为登录流,此组件将显示一个表单,用于从用户收集一次性密码。

  4. 如果选择的身份验证方法是电子邮件和密码,请创建 登录/组件/PasswordForm.tsx 文件,然后粘贴 登录/组件/PasswordForm.tsx 中的代码。 此组件显示一个用于收集用户密码的表单。

  5. 创建 登录/组件/UserInfo.tsx 文件,然后粘贴 登录/组件/UserInfo.tsx 中的代码。 此组件显示已登录用户的用户名和登录状态。

处理表单交互

在本部分中,将添加用于处理登录表单交互的代码,例如启动登录流、提交用户密码或一次性密码。

创建 登录/page.tsx 文件以处理登录流的逻辑。 在此文件中:

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

    import {
      CustomAuthPublicClientApplication,
      ICustomAuthPublicClientApplication,
      SignInCodeRequiredState,
      // Uncommon if using a Email + Password flow
      // SignInPasswordRequiredState,
      SignInCompletedState,
      AuthFlowStateBase,
    } from "@azure/msal-browser/custom-auth";
    
    export default function SignIn() {
        const [authClient, setAuthClient] = useState<ICustomAuthPublicClientApplication | null>(null);
        const [username, setUsername] = useState("");
        //If you are sign in using a Email + Password flow, uncomment the following line
        //const [password, setPassword] = useState("");
        const [code, setCode] = useState("");
        const [error, setError] = useState("");
        const [loading, setLoading] = useState(false);
        const [signInState, setSignInState] = useState<AuthFlowStateBase | null>(null);
        const [data, setData] = useState<CustomAuthAccountData | undefined>(undefined);
        const [loadingAccountStatus, setLoadingAccountStatus] = useState(true);
        const [isSignedIn, setCurrentSignInStatus] = 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()) {
                    setCurrentSignInStatus(true);
                }
    
                setData(accountResult.data);
    
                setLoadingAccountStatus(false);
            };
    
            checkAccount();
        }, [authClient]);
    
        const renderForm = () => {
            if (loadingAccountStatus) {
                return;
            }
    
            if (isSignedIn || signInState instanceof SignInCompletedState) {
                return <UserInfo userData={data} />;
            }
            //If you are signing up using Email + Password flow, uncomment the following block of code
            /*
            if (signInState instanceof SignInPasswordRequiredState) {
                return (
                    <PasswordForm
                        onSubmit={handlePasswordSubmit}
                        password={password}
                        setPassword={setPassword}
                        loading={loading}
                    />
                );
            }
            */
            if (signInState instanceof SignInCodeRequiredState) {
                return <CodeForm onSubmit={handleCodeSubmit} code={code} setCode={setCode} loading={loading} />;
            }
    
            return <InitialForm onSubmit={startSignIn} username={username} setUsername={setUsername} loading={loading} />;
        };
    
        return (
            <div style={styles.container}>
                <h2 style={styles.h2}>Sign In</h2>
                <>
                    {renderForm()}
                    {error && <div style={styles.error}>{error}</div>}
                </>
            </div>
        );
    }
    
  • 若要启动登录流,请使用以下代码片段。 请参阅 登录/page.tsx 的完整示例,了解如何放置代码片段的位置:

    const startSignIn = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (!authClient) return;
    
        // Start the sign-in flow
        const result = await authClient.signIn({
            username,
        });
    
        // Thge result may have the different states,
        // such as Password required state, OTP code rquired state, Failed state and Completed state.
    
        if (result.isFailed()) {
            if (result.error?.isUserNotFound()) {
                setError("User not found");
            } else if (result.error?.isInvalidUsername()) {
                setError("Username is invalid");
            } else if (result.error?.isPasswordIncorrect()) {
                setError("Password is invalid");
    
            } else {
                setError(`An error occurred: ${result.error?.errorData?.errorDescription}`);
            }
        }
    
        if (result.isCompleted()) {
            setData(result.data);
        }
    
        setSignInState(result.state);
    
        setLoading(false);
    };
    

    SDK 的实例方法 signIn()启动登录流。

    注释

    在租户的用户流中启用username内置用户属性时,该参数接受用户的电子邮件地址或其用户名(别名)。 用户可以输入要登录的任一值。 若要启用该属性,请参阅 登录标识符策略中的“启用用户名”。

  • 如果选择的身份验证流是电子邮件和一次性密码,请使用以下代码片段提交一次性密码。 请参阅 登录/page.tsx 的完整示例,了解如何放置代码片段的位置:

    const handleCodeSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (signInState instanceof SignInCodeRequiredState) {
            const result = await signInState.submitCode(code);
    
            // the result object may have the different states, such as Failed state and Completed state.
    
            if (result.isFailed()) {
                if (result.error?.isInvalidCode()) {
                    setError("Invalid code");
                } else {
                    setError(result.error?.errorData?.errorDescription || "An error occurred while verifying the code");
                }
            }
    
            if (result.isCompleted()) {
                setData(result.data);
                setSignInState(result.state);
            }
        }
    
        setLoading(false);
    };
    

    登录状态 submitCode() 会提交一次性验证码。

  • 如果选择的身份验证流是电子邮件和密码,请使用以下代码片段提交用户的密码。 请参阅 登录/page.tsx 的完整示例,了解如何放置代码片段的位置:

    const handlePasswordSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setError("");
        setLoading(true);
    
        if (signInState instanceof SignInPasswordRequiredState) {
            const result = await signInState.submitPassword(password);
    
            if (result.isFailed()) {
                if (result.error?.isInvalidPassword()) {
                    setError("Incorrect password");
                } else {
                    setError(
                        result.error?.errorData?.errorDescription || "An error occurred while verifying the password"
                    );
                }
            }
    
            if (result.isCompleted()) {
                setData(result.data);
    
                setSignInState(result.state);
            }
        }
    
        setLoading(false);
    };
    

    登录状态 submitPassword() 会提交用户的密码。

处理注册错误

登录过程中,并非所有操作都会成功。 例如,用户可能尝试使用不存在的用户名登录,或者提交无效的电子邮件一次性密码或不符合最低要求的密码。 请确保在以下情况下正确处理错误:

  • signIn() 方法中启动登录流。

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

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

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

运行并测试应用

使用 运行和测试应用程序中的步骤来运行您的应用程序,然后测试登录流程。