適用於:灰色 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元件
在登入流程期間,此應用程式會收集使用者的認證、使用者名稱(電子郵件)和密碼,以登入使用者。 使用者成功登入之後,應用程式會顯示使用者的詳細數據。
在 src 資料夾中建立名為 /pages/signin 的資料夾。
若要建立、顯示及提交登入表單,請建立一個檔案 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> ); };
若要在成功登入之後顯示使用者的詳細資料:
建立名為 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); }
在 src/pages 資料夾中建立名為 使用者 的資料夾。
建立名為 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>
);
};
執行及測試您的應用程式
使用 執行並測試您的應用程式 中的步驟來執行您的應用程式,但這次,請使用您稍早註冊的使用者帳戶來測試登入流程。