如何在React应用中使用Redux来管理认证的状态

217 阅读9分钟

React有无数的状态管理选项。当你有一个共享状态的嵌套组件树时,React提供了使用内置Context的选项。还有一个内置的useState钩子,允许你为一个组件设置本地状态。

对于更复杂的情况,即你需要一个经常变化并在你的应用程序的大部分部分共享的单一真相来源,可以考虑使用一个更强大的状态管理库。

本攻略将让你开始使用Okta的重定向模型进行基本设置,以管理React应用中的认证状态和用户配置文件信息。它提供了关于何时使用Redux和本地状态的例子,使用React的useState 钩子和React Context。

前提条件

截至本出版物发布之时,Create React App需要Node >= 14.0.0和npm >= 5.6。你可以在reactjs.org/docs/create… 检查最新的所需版本。

使用TypeScript创建React应用

我们将从内置的React模板开始,创建使用Redux和TypeScript的React应用。这包括推荐的Redux工具箱和Redux与React组件的集成。你也可以克隆GitHub repo,按照那里的说明进行配置,而不是构建项目。

  1. 首先,运行

    npx create-react-app okta-react-redux --template redux-typescript
    

注意:我们的演示资源库使用React^18.2.0 和React Scripts5.0.1

  1. 然后我们通过运行来添加Redux的核心:

    npm add redux@4.2
    
  2. Redux提供了它自己的类型,但我们要添加我们的react-redux ,因为我们使用的是TypeScript:

    npm add -D @types/react-redux@7.1
    
  3. 我们还需要为我们的应用程序提供路由(和路由类型):

    npm install react-router-dom@5
    npm add -D @types/react-router-dom@5.3
    
  4. 要启动你的应用程序,请运行:

    npm start
    

Redux如何工作

要使用Redux更新状态,需要派发一个动作。然后商店使用根还原器来计算与旧状态相比的新状态,并将更新通知给适当的订阅者,以便UI可以正确更新。

一个典型的Redux工作流程是这样的。首先,你的应用程序的用户界面是使用一个初始的全局Redux商店渲染的,该商店是用根减速器创建的:

  1. 在你的应用程序的UI中发生了一个事件,例如用户的按钮交互,这就触发了一个动作。
  2. 然后,该动作被派发到存储,处理所需逻辑的事件处理器对你的全局状态进行更改。
  3. 存储器会通知UI中的订阅部分有一个更新。
  4. 这些订阅的部分然后检查是否有必要重新渲染,以根据需要更新UI。

注意:我们使用的内置Redux模板包括一个很好的反面例子,说明Redux是如何工作的。你可以仔细看看Redux对计数器例子的解释

使用Redux的有用术语

动作

动作是一个JS对象,有一个类型和一个有效载荷。它描述了你的应用程序中的一个可操作的互动,通常被命名为createUseraddToDo

动作创建者

动作创建者可以用来动态地创建动作。我们的演示没有使用动作创建器,但你可以在上面的链接中阅读更多关于它的信息。

减速器

当传递一个新的状态时,还原器通过与之前的状态进行比较来返回适当的当前状态,并做出相应的改变。还原器的工作原理与Array.reduce 方法类似。

Redux存储

你的应用程序的当前状态存在于Redux商店中。

选择器

选择器返回存储在Redux商店中的Redux实时状态的一个部分。

派遣

Dispatch是一个触发行动的方法,它反过来更新Redux商店。

使用OAuth2和OpenID Connect(OIDC)添加认证

对于这个演示应用程序,我们将使用Okta的SPA重定向模型来验证和获取用户信息。

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,注册一个新的账户。如果你已经有一个账户,运行okta login 。然后,运行okta apps create 。选择默认的应用程序名称,或根据你的需要进行更改。 选择单页应用程序,然后按回车键

使用http://localhost:3000/login/callback 作为重定向URI,并接受默认的注销重定向URI:http://localhost:3000/login

Okta CLI是做什么的?

Okta CLI将在您的Okta机构中创建一个OIDC单页应用。它将添加您指定的重定向URI,并授予Everyone组的访问权。它还会为http://localhost:3000/login 添加一个受信任的来源。当它完成时,你会看到如下的输出。

Okta application configuration:
Issuer:    https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6

注意:你也可以使用Okta管理控制台来创建你的应用程序。更多信息请参见创建一个React应用程序

  1. 通过运行Okta SDKs添加到你的项目中:

    npm install @okta/okta-react@6.4 @okta/okta-auth-js@6.0
    

使用Redux状态管理的TypeScript React应用程序设置

  1. 在你的根目录下创建一个.env 文件,并添加以下内容:

    REACT_APP_OKTA_ISSUER=https://{yourOktaDomain}/oauth2/default
    REACT_APP_OKTA_CLIENTID={yourOktaClientId}
    REACT_APP_OKTA_BASE_REDIRECT_URI=http://localhost:3000
    

注意:为了安全起见,记得不要把你的.env 文件包括在任何版本控制中。

添加认证并创建路由

修改现有的App.tsx 文件:

import "./App.css";
import { OktaAuth, toRelativeUrl } from "@okta/okta-auth-js";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { LoginCallback, Security } from "@okta/okta-react";
import Home from "./components/home";
import { useCallback } from "react";

const oktaAuth = new OktaAuth({
  issuer: process.env.REACT_APP_OKTA_ISSUER,
  clientId: process.env.REACT_APP_OKTA_CLIENTID,
  redirectUri: process.env.REACT_APP_OKTA_BASE_REDIRECT_URI + "/login/callback",
});

function App() {
  const restoreOriginalUri = useCallback(
    async (_oktaAuth: OktaAuth, originalUri: string) => {
      window.location.replace(
        toRelativeUrl(originalUri || "/", window.location.origin)
      );
    },
    []
  );

  return (
    <Router>
      <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
        <Route path="/" exact={true} component={Home} />
        <Route path="/login/callback" component={LoginCallback} />
      </Security>
    </Router>
  );
}

export default App;

添加初始状态,创建Redux slice,并创建选择器

  1. 创建一个src/redux-state 目录:

    mkdir src/redux-state
    
  2. 在创建的redux-state 目录中添加一个userProfileSlice.tsx 文件,内容如下:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../app/store";

export interface IUserProfile {
  email: string;
  given_name: string;
  family_name: string;
}

const initialState: IUserProfile = {
  email: "",
  given_name: "",
  family_name: "",
};

export const userProfileSlice = createSlice({
  name: "userProfile",
  initialState,
  reducers: {
    setUserProfile: (state, action) => {
      return {
        email: action.payload.payload.email,
        given_name: action.payload.payload.given_name,
        family_name: action.payload.payload.family_name,
      };
    },
  },
});

export const selectUserProfile = (state: RootState): IUserProfile =>
  state.userProfile;

export const { setUserProfile } = userProfileSlice.actions;
export default userProfileSlice.reducer;
  1. src/app/store.ts 文件中,将创建的Redux slice添加到Redux商店中:
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import userProfileReducer from "../redux-state/userProfileSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    userProfile: userProfileReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

用状态管理显示用户资料信息

  1. 创建一个src/components 目录:

    mkdir src/components
    
  2. src/components 目录中,添加以下文件:

    home.tsx

import { useOktaAuth } from "@okta/okta-react";
import { createContext, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { setUserProfile } from "../redux-state/userProfileSlice";
import "../App.css";
import Dashboard from "./dashboard";
import { UserClaims } from "@okta/okta-auth-js";

type UserProfileExtra = Pick<UserClaims, "locale" | "preferred_username">;

const emptyUserContext = {
  preferred_username: "",
  locale: "",
} as UserProfileExtra;

export const UserContext = createContext(emptyUserContext);

export default function Home() {
  const dispatch = useDispatch();
  const { oktaAuth, authState } = useOktaAuth();
  const [userProfileExtra, setUserProfileExtra] = useState<UserProfileExtra>();

  const login = async () => oktaAuth.signInWithRedirect();

  useEffect(() => {
    if (authState?.isAuthenticated) {
      oktaAuth
        .getUser()
        .then((userInfo: UserProfileExtra) => {

          dispatch(
            setUserProfile({
              type: "userProfile/userProfileSet",
              payload: userInfo,
            })
          );

          setUserProfileExtra({
            locale: userInfo.locale,
            preferred_username: userInfo.preferred_username,
          });
        })
        .catch((err) => {
          console.error(err);
        });
    }
  }, [authState?.isAuthenticated, dispatch, oktaAuth]);

  return (authState?.isAuthenticated && userProfileExtra) ? (
    <UserContext.Provider value={userProfileExtra}>
      <Dashboard />
    </UserContext.Provider>
  ) : (
    <div className="section-wrapper">
      <div className="title">Use Redux to Manage Authenticated State in a React App</div>
      <button className="button" onClick={login}>
        Login
      </button>
    </div>
  );
}

这里我们使用React的useEffect 钩子来设置我们之前用setUserProfile 设置的全局用户状态和我们在这个文件中用 设置的本地用户状态。 useState.

一旦用户被认证,我们依靠内置在oktaAuth 中的getUser 方法来获取用户档案信息。如果用户还没有通过认证,我们就渲染登录页面。

dashboard.tsx

import { useOktaAuth } from "@okta/okta-react";
import { useSelector } from "react-redux";
import { selectUserProfile } from "../redux-state/userProfileSlice";
import "../App.css";
import { useState } from "react";
import UserProfile from "./userProfile";
import UserProfileExtra from "./userProfileExtra";

export default function Dashboard() {
  const { oktaAuth } = useOktaAuth();
  const userProfile = useSelector(selectUserProfile);
  const [isExpanded, setIsExpanded] = useState(false);

  const logout = async () => oktaAuth.signOut();

  return (
    <div className="section-wrapper">
      <div className="title">Dashboard</div>
      <div className="profile-greeting">{`Hi ${userProfile.given_name}!`}</div>
      <div className="profile-more-wrapper">
        {isExpanded && (
          <>
            <UserProfile />
            <UserProfileExtra />
          </>
        )}
      </div>
      {
        <div
          className="profile-toggle"
          onClick={() => setIsExpanded(!isExpanded)}
        >
          {isExpanded ? "Show less" : "Show more"}
        </div>
      }
      <div>
        <button className="button" onClick={logout}>
          Logout
        </button>
      </div>
    </div>
  );
}

在这个文件中,我们使用一个Redux选择器来获取userProfile 的当前状态,然后在我们的组件中渲染这些值。

我们还设置了一个isExpandable 属性来切换是否隐藏或显示额外的用户资料信息。这是另一个关于如何使用本地状态与useState 钩子的例子。

userProfile.tsx

import { useSelector } from "react-redux";
import { selectUserProfile } from "../redux-state/userProfileSlice";
import "../App.css";

export default function UserProfile() {
  const userProfile = useSelector(selectUserProfile);

  return (
    <>
      <div>
        <span>Email: </span>
        {userProfile.email}
      </div>
      <div>
        <span>Last name: </span>
        {userProfile.family_name}
      </div>
    </>
  );
}

在这个文件中,我们使用之前创建的Redux选择器来获取状态片的当前状态。这包括我们之前设置的用户配置文件信息。

userProfileExtra.tsx

import "../App.css";
import { useContext } from "react";
import { UserContext } from "./home";

export default function UserProfileExtra() {
  const userProfileExtra = useContext(UserContext);

  return (
    <>
      <div>
        <span>Username: </span>
        {userProfileExtra.preferred_username}
      </div>
      <div>
        <span>Locale: </span>
        {userProfileExtra.locale}
      </div>
    </>
  );
}

在这里,我们展示了另一种管理状态的方法。这一次,React内置的useContext 钩子被用来获取我们之前用React的createContext 方法创建的UserContext 中的设置。

注意:下面的自定义样式也已经被添加到演示资源库中的src/App.css

.section-wrapper > div {
  padding: 24px;
  font-family: Roboto, sans-serif;
}

.section-wrapper {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: calc(100vh - 100vh * 0.33);
  margin-top: 56px;
}

.button {
  min-width: 64px;
  height: 36px;
  border-color: #6200ee;
  color: #6200ee;
  background-color: transparent;
  border-style: solid;
  padding: 0 15px 0 15px;
  border-width: 1px;
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.0892857143em;
  text-transform: uppercase;
  border-radius: 4px;
  border-style: solid;
  padding: 0 15px 0 15px;
  border-width: 1px;
}

.button:hover {
  cursor: pointer;
  background-color: rgb(112, 76, 182, 0.1);
}

.title {
  font-size: 48px;
  text-align: center;
}

.profile-greeting {
  font-size: 32px;
}

.profile-toggle {
  font-size: 16px;
  text-decoration: underline;
  color: #6200ee;
}

.profile-more-wrapper {
  height: 360px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.profile-more-wrapper > div {
  padding: 16px;
}

.profile-more-wrapper span {
  font-size: 20px;
  font-weight: 500;
}

我们的应用程序现在已经完成了!接下来我们将运行它来登录用户并渲染我们的适用状态。

运行该应用程序

要启动该应用程序,请运行npm start

在你的应用程序启动后,你在主屏幕上点击login ,你将被重定向到Okta托管的登录页面。在这里,输入你的用户凭证。一旦你的用户被认证,我们创建的仪表板组件就会呈现。

如果你还记得,用户的值given_name ,是我们创建的选择器selectUserProfile 的Redux分片的一部分。点击Show moreShow less ,会切换我们在仪表盘组件中的isExpanded 的本地状态。这允许我们显示或隐藏额外的用户配置文件信息,这是一个来自userProfileSlice 和我们创建的UserContext 的额外值的混合。

了解更多关于认证、React和Redux的信息

在这篇文章中,我们使用Okta的重定向模型来管理React应用中的认证状态和用户资料信息。我们还看了关于何时使用Redux、使用React的useState 钩的本地状态或React上下文的例子。当你扩展你的应用程序时,你可能会发现这三种解决方案的混合是正确的选择。

你可能还想进一步处理更复杂的状态管理逻辑,或者为你的应用添加更多的定制。下面的链接应该对潜在的下一步有所帮助。