共用方式為


教學課程:使用原生驗證將使用者登入 React 單頁應用程式 (預覽)

適用於:灰色 X 符號的白色圓圈 。 職場租戶 白色勾選符號的綠色圓圈。 外部租戶(深入瞭解

在本教學課程中,您將瞭解如何使用原生驗證將使用者登入 React 單頁應用程式 (SPA)。

在本教學課程中,您會:

  • 更新 React 應用程式,以使用使用者名稱(電子郵件)和密碼登入使用者。
  • 測試登入流程。

先決條件

定義應用程式對原生驗證 API 進行呼叫的類型

在登入流程期間,應用程式會多次呼叫原生驗證 API,例如啟動登入要求、選取驗證方法,以及要求安全性令牌。

若要定義這些呼叫,請開啟 scr/client/RequestTypes.ts 檔案,然後附加下列代碼段:

   export interface TokenRequestType {
        continuation_token: string;
        client_id: string;
        grant_type: string;
        scope: string;
        password?: string;
        oob?: string;
        challenge_type?: string;
    }

    // Sign in
    export interface TokenSignInType {
        continuation_token: string;
        grant_type: string;
        password?: string;
        oob?: string;
    }

    export interface ChallengeRequest {
        client_id: string;
        challenge_type: string;
        continuation_token: string;
    }

    export interface SignInStartRequest {
        client_id: string;
        challenge_type: string;
        username: string;
    }

    export interface SignInTokenRequest {
        client_id: string;
        grant_type: string;
        continuation_token: string;
        scope: string;
        challenge_type?: string;
        password?: string;
        oob?: string;
    }

定義從原生驗證 API 接收的回應應用程式類型

若要定義應用程式可從原生驗證 API 接收以進行登入作業的回應類型,請開啟 src/client/ResponseTypes.ts 檔案,然後附加下列代碼段:

    export interface TokenResponseType {
        token_type: string;
        scope: string;
        expires_in: number;
        access_token: string;
        refresh_token: string;
        id_token: string;
    }

處理登入要求

在本節中,您會新增程式碼來處理登入流程要求。 這些要求的範例包括啟動登入流程、選取驗證方法或要求安全性令牌。

若要這樣做,請建立名為 src/client/SignInService.ts的檔案,然後新增下列代碼段:

    import { CLIENT_ID, ENV } from "../config";
    import { postRequest } from "./RequestClient";
    import { ChallengeRequest, SignInStartRequest, TokenRequestType, TokenSignInType } from "./RequestTypes";
    import { TokenResponseType } from "./ResponseTypes";

    export const signInStart = async ({ username }: { username: string }) => {
        const payloadExt: SignInStartRequest = {
            username,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
        };

        return await postRequest(ENV.urlOauthInit, payloadExt);
    };

    export const signInChallenge = async ({ continuation_token }: { continuation_token: string }) => {
        const payloadExt: ChallengeRequest = {
            continuation_token,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
        };

        return await postRequest(ENV.urlOauthChallenge, payloadExt);
    };

    export const signInTokenRequest = async (request: TokenSignInType): Promise<TokenResponseType> => {
        const payloadExt: TokenRequestType = {
            ...request,
            client_id: CLIENT_ID,
            challenge_type: "password oob redirect",
            scope: "openid offline_access",
        };

        if (request.grant_type === "password") {
            payloadExt.password = request.password;
        }

        if (request.grant_type === "oob") {
            payloadExt.oob = request.oob;
        }

        return await postRequest(ENV.urlOauthToken, payloadExt);
    };

challenge_type 屬性會顯示用戶端應用程式支援的驗證方法。 此應用程式會使用電子郵件和密碼進行登入,因此挑戰類型值為 密碼 oob 重新導向。 深入瞭解 挑戰類型

建立UI元件

在登入流程期間,此應用程式會收集使用者的認證、使用者名稱(電子郵件)和密碼,以登入使用者。 使用者成功登入之後,應用程式會顯示使用者的詳細數據。

  1. src 資料夾中建立名為 /pages/signin 的資料夾。

  2. 若要建立、顯示及提交登入表單,請建立一個檔案 src/pages/signin/SignIn.tsx,然後新增下列程式代碼:

        import React, { useState } from "react";
        import { Link as LinkTo, useNavigate } from "react-router-dom";
        import { signInStart, signInChallenge, signInTokenRequest } from "../../client/SignInService";
        import { ErrorResponseType } from "../../client/ResponseTypes";
    
        export const SignIn: React.FC = () => {
          const [email, setEmail] = useState<string>("");
          const [password, setPassword] = 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 (!validateEmail(email)) {
              setError("Invalid email format");
              return;
            }
            setError("");
            setIsloading(true);
            try {
              const res1 = await signInStart({
                username: email,
              });
              const res2 = await signInChallenge({ continuation_token: res1.continuation_token });
              const res3 = await signInTokenRequest({
                continuation_token: res2.continuation_token,
                grant_type: "password",
                password: password,
              });
              navigate("/user", { state: res3 });
            } catch (err) {
              setError("An error has occured " + (err as ErrorResponseType).error_description);
            } finally {
              setIsloading(false);
            }
          };
    
          return (
            <div className="login-form">
              <form onSubmit={handleSubmit}>
                <h2>Login</h2>
                <div className="form-group">
                  <label>Email:</label>
                  <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
                </div>
                <div className="form-group">
                  <label>Password:</label>
                  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
                </div>
                {error && <div className="error">{error}</div>}
                {isLoading && <div className="warning">Sending request...</div>}
                <button type="submit" disabled={isLoading}>Login</button>
              </form>
            </div>
          );
        };
    
  3. 若要在成功登入之後顯示使用者的詳細資料:

    1. 建立名為 client/Utils.ts的檔案,然後新增下列代碼段:

          export function parseJwt(token: string) {
              var base64Url = token.split(".")[1];
              var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
              var jsonPayload = decodeURIComponent(
                  window
                  .atob(base64)
                  .split("")
                  .map(function (c) {
                      return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
                  })
                  .join("")
              );
              return JSON.parse(jsonPayload);
          }
      
    2. src/pages 資料夾中建立名為 使用者 的資料夾。

    3. 建立名為 src/pages/user/UserInfo.tsx的檔案,然後新增下列代碼段:

      // User.tsx
      import React from "react";
      import { useLocation } from "react-router-dom";
      import { parseJwt } from "../../client/Utils";
      
      export const UserInfo: React.FC = () => {
        const { state } = useLocation();
        const decodedToken = parseJwt(state.access_token);
        const { given_name, scp, family_name, unique_name: email } = decodedToken;
      
        console.log(decodedToken);
        const familyName = family_name;
        const givenName = given_name;
        const tokenExpireTime = state.expires_in;
        const scopes = state.scope;
      
        return (
          <div className="user-info">
            <h2>User Information</h2>
            <div className="info-group">
              <label>Given Name:</label>
              <span>{givenName}</span>
            </div>
            <div className="info-group">
              <label>Family Name:</label>
              <span>{familyName}</span>
            </div>
            <div className="info-group">
              <label>Email:</label>
              <span>{email}</span>
            </div>
            <div className="info-group">
              <label>Token Expire Time:</label>
              <span>{tokenExpireTime}</span>
            </div>
            <div className="info-group">
              <label>Scopes:</label>
              <span>{scopes}</span>
            </div>
            <div className="info-group">
              <label>Token payload:</label>
              <span><pre>{JSON.stringify(decodedToken, null, 2)}</pre></span>
            </div>
          </div>
        );
      };
      

新增應用程式路由

開啟 src/AppRoutes.tsx 檔案,然後使用下列程式代碼取代其內容:

import { Route, Routes } from "react-router-dom";
import { SignIn } from "./pages/SignIn/SignIn";
import { UserInfo } from "./pages/User/UserInfo";
import { SignUp } from "./pages/SignUp/SignUp";
import { SignUpChallenge } from "./pages/SignUp/SignUpChallenge";
import { SignUpCompleted } from "./pages/SignUp/SignUpCompleted";
//For password reset
//import { ResetPassword } from "./pages/ResetAccount/ResetPassword";

export const AppRoutes = () => {
  return (
    <Routes>
      <Route path="/" element={<SignUp />} />
      <Route path="/signin" element={<SignIn />} />
      <Route path="/user" element={<UserInfo />} />
      <Route path="/signup" element={<SignUp />} />
      <Route path="/signup/challenge" element={<SignUpChallenge />} />
      <Route path="/signup/completed" element={<SignUpCompleted />} />
      //For password reset
      //<Route path="/reset" element={<ResetPassword />} />
    </Routes>
  );
};

執行及測試您的應用程式

使用 執行並測試您的應用程式 中的步驟來執行您的應用程式,但這次,請使用您稍早註冊的使用者帳戶來測試登入流程。

下一步