跳转至

React

Authany 可以帮助您为 React 应用添加用户登录功能,它提供了预置的登录页面和用户设置页面,能够加快您的开发进度。

跟随这个 🕐 15 分钟教程,使用 React 和 Authany SDK 创建一个简单的应用。

说明

查看并克隆 GitHub 上的示例项目。

在 Authany 中设置应用

要使用 Authany 的功能,您需要一个账号和一个项目。请在 https://manage.authany.com/ 免费注册账号,并创建一个新项目以开始使用。

之后,我们需要在项目管理台中创建一个应用。

在管理台中创建应用

要创建客户端应用,请在 Authany 管理台左侧菜单栏中进入应用。

在管理台中导航到应用页面

接下来,在顶部工具栏中点击 ⊕新增应用。

输入应用名称,例如 “MyAwesomeApp”,然后将应用类型选择为单页应用。点击保存按钮创建应用。

配置授权重定向 URI

授权重定向 URI 是应用中的一个 URL,用户使用 Authany 登录后会被重定向到该地址。您需要在这个路径中调用完成身份验证(finish authentication)功能,完成登录流程。

打开刚创建的 Authany 客户端应用的 URI 部分,添加一个新的授权重定向 URI。本教程请将 http://localhost:4000/auth-redirect 添加到授权重定向 URI 列表中。

在进行下一步之前,点击保存以保留所有客户端应用配置的更改。

配置授权重定向 URI

将 Authany 添加到 React 应用

在本节中,我们将创建一个简单的 React 应用,并将它与 Authany 连接,让应用的用户可以登录、查看用户设置并登出账号。

步骤 1:创建一个简单的 React 项目

以下是搭建 React 项目脚手架的推荐步骤。如果您是将 Authany 添加到现有项目中,可以跳过这部分,直接查看下一节的步骤 2:向项目安装 Authany SDK。

使用 Vite 创建一个新的 React 项目

在您喜欢的文件夹中运行以下命令,使用 Vite 创建一个新的 React 项目:

npm create vite@latest my-app -- --template react-ts 

接下来,运行以下命令进入新项目目录并安装依赖:

cd my-app
npm install

修改端口

在 package.json 文件中,将 script.dev 部分里 dev 字段的值改为:

"dev": "vite --port 4000",

这样可以让 npm run dev 命令在开发模式下于 4000 端口运行应用。

现在,您项目的文件结构应如下所示:

my-app
├── node_modules
│   └── (...)
├── package-lock.json
├── package.json
├── index.html
└── src
    ├── App.tsx
    └── main.tsx

现在运行 npm run dev 启动项目,在浏览器中打开 http://localhost:4000 后,您将看到默认的 “Vite + React” 页面。

步骤 2:向项目安装 Authany SDK

在您的 React 项目目录中运行以下命令,安装 Authany Web SDK

npm install --save --save-exact @authgear/web

在 src/main.tsx 中,导入 authgear 并调用 configure 函数,在应用加载时初始化一个 Authany 实例。

import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import authgear from "@authgear/web"

async function init() {
  try {
    // configure Authgear container instance
    await authgear.configure({
      endpoint: import.meta.env.VITE_AUTHGEAR_ENDPOINT,
      clientID: import.meta.env.VITE_AUTHGEAR_CLIENT_ID,
      sessionType: "refresh_token",
    });
  } finally {
    createRoot(document.getElementById("root")!).render(<App />);
  }
}

init().catch((e) => {
  // Error handling
  console.error(e)
});

Authany 容器实例接受 endpoint 和 clientID 作为参数,您可以在在 Authany 中设置应用一节所创建应用的配置页面中获取这两个值。请在项目根目录下创建一个 .env 文件,并使用以下字段添加您的 Authany 客户端应用配置:

VITE_AUTHGEAR_CLIENT_ID=<CLIENT_ID>
VITE_AUTHGEAR_ENDPOINT=<AUTHGEAR_ENDPOINT>
VITE_AUTHGEAR_REDIRECT_URL=http://localhost:4000/auth-redirect

建议在 configure() 完成后再渲染应用,这样应用渲染时 Authany 已经可以使用。

说明

现在运行 npm run dev,如果 Authany SDK 配置成功,您应该会再次看到默认页面,且控制台没有报错信息

步骤 3:实现 Context Provider

由于我们希望在应用的各个位置引用登录状态,让我们把这个状态放进一个 context provider 中,也就是 /src/context 文件夹下的 UserProvider.tsx。

UserProvider.tsx 文件中会有一个 isLoggedIn 布尔值和一个 setIsLoggedIn 函数。isLoggedIn 这个布尔状态可以通过 onSessionStateChange 回调自动更新,该回调可以存放在本地 SDK 容器的 delegate 中。

// src/context/UserProvider.tsx
import React, { createContext, useEffect, useState, useMemo } from "react";
import authgear from "@authgear/web";

interface UserContextValue {
  isLoggedIn: boolean;
}

export const UserContext = createContext<UserContextValue>({
  isLoggedIn: false,
});

interface UserContextProviderProps {
  children: React.ReactNode;
}

const UserContextProvider: React.FC<UserContextProviderProps> = ({
  children,
}) => {
  // By default the user is not logged in
  const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);

  useEffect(() => {
    // When the sessionState changed, logged in state will also be changed
    authgear.delegate = {
      onSessionStateChange: (container) => {
        // sessionState is now up to date
        // Value of sessionState can be "NO_SESSION" or "AUTHENTICATED"
        const sessionState = container.sessionState;
        if (sessionState === "AUTHENTICATED") {
          setIsLoggedIn(true);
        } else {
          setIsLoggedIn(false);
        }
      },
    };
  }, [setIsLoggedIn]);

  const contextValue = useMemo<UserContextValue>(() => {
    return {
      isLoggedIn,
    };
  }, [isLoggedIn]);

  return (
    <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>
  );
};

export default UserContextProvider;

步骤 4:实现 Auth Redirect

接下来,我们将添加一个 “AuthRedirect” 页面,用来处理用户通过 Authany 完成身份验证后的结果。

首先,使用以下命令安装 react-router-dom:

npm install --save-exact react-router-dom

在 src/ 文件夹中创建 AuthRedirect.tsx 组件文件。

在 Auth Redirect 组件中调用 Authany 的 finishAuthentication() 函数,将令牌发送回 Authany 服务器以换取访问令牌和刷新令牌。不用担心这些技术细节,finishAuthentication() 会替您完成全部工作,并保存身份验证数据。

身份验证完成后,UserContextProvider 中的 isLoggedIn 状态会自动被设为 true。最后,跳转回根路径(/),也就是我们的 Home 页面。

最终的 AuthRedirect.tsx 如下所示

// src/AuthRedirect.tsx
import React, { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import authgear from "@authgear/web";

const AuthRedirect: React.FC = () => {
  const usedToken = useRef(false);

  const navigate = useNavigate();

  useEffect(() => {
    async function updateToken() {
      try {
        await authgear.finishAuthentication();
      } finally {
        navigate("/");
        usedToken.current = true;
      }
    }

    if (!usedToken.current) {
      updateToken().catch((e) => console.error(e));
    }
  }, [navigate]);

  return <></>;
};

export default AuthRedirect;

说明

由于在 React 18 及更高版本中,useEffect 在开发模式下会被触发两次,我们需要实现一个清理函数来防止它被触发两次。我们将使用 useRef Hook 来防止用户令牌被发送给 Authany 端点两次。

如果没有清理函数,useEffect Hook 会被触发两次,因此 finishAuthentication() 会把令牌发送给 Authany 端点两次,其中第二次会因为令牌只能使用一次而导致 “Invalid Token” 错误。

步骤 5:向应用添加路由和 Context Provider

现在,我们将添加一个 “Home” 页面。在 src/ 文件夹中创建 Home.tsx 组件文件。

然后导入 Home 和 AuthRedirect 作为路由,并导入 UserContextProvider 用它包裹这些路由。

最终的 App.tsx 应如下所示:

// src/App.tsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './Home';
import AuthRedirect from './AuthRedirect';
import UserContextProvider from './context/UserProvider';

const App: React.FC = () => {
  return (
    <UserContextProvider>
      <Router>
        <Routes>
          <Route path="/auth-redirect" element={<AuthRedirect />} />
          <Route path="/" element={<Home />} />
        </Routes>
      </Router>
    </UserContextProvider>
  );
}

export default App;

此时的文件结构应如下所示

src
├── App.tsx
├── AuthRedirect.tsx
├── Home.tsx
├── context
│   └── UserProvider.tsx
└── main.tsx

步骤 6:添加登录按钮

首先,我们将把 Authany 依赖和要用到的 React Hook 导入 Home.tsx。然后添加登录按钮,它会通过 startLogin 的点击回调调用 startAuthentication(ConfigureOptions),从而将用户重定向到登录页面。

// src/Home.tsx
import React, { useEffect, useState, useCallback, useContext } from 'react';
import authgear, { PromptOption } from '@authgear/web';

const Home: React.FC = () => {
  const startLogin = useCallback(() => {
    authgear
      .startAuthentication({
        redirectURI: import.meta.env.VITE_AUTHGEAR_REDIRECT_URL,
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authentication, user should be redirected to Authgear
        },
        err => {
          // failed to start authentication
        }
      );
  }, []);
  return (
    <div>
      <h1>Home Page</h1>
      <div>
        <button onClick={startLogin}>Login</button>
      </div>
    </div>
  );
}

export default Home;

现在运行 npm run dev,点击登录按钮后,您将被重定向到 Authany 登录页面。

点击登录按钮后,用户将被重定向到 Authany 登录页面

步骤 7:显示用户信息

Authany SDK 可以帮助您轻松获取已登录用户的信息。

在上一步中,用户已经成功登录,现在让我们尝试在 Home 页面上打印用户的用户 ID(sub)。

在 Home.tsx 中,我们将添加一个简单的加载界面和一条打印 Sub ID 的问候语。我们会添加两个条件渲染元素,让它们仅在用户已登录时显示;也可以让登录按钮只在用户未登录时显示。

使用 UserContext 中的 isLoggedIn 来控制页面上的组件,通过 fetchUserInfo() 获取用户信息,并访问其 sub 属性。

// src/Home.tsx  
import React, { useEffect, useState, useCallback, useContext } from "react";
import authgear, { PromptOption } from "@authgear/web";
import { UserContext } from "./context/UserProvider";

const Home: React.FC = () => {
  const [greetingMessage, setGreetingMessage] = useState<string>("");
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const { isLoggedIn } = useContext(UserContext);

  useEffect(() => {
    async function updateGreetingMessage() {
      setIsLoading(true);
      try {
        if (isLoggedIn) {
          const userInfo = await authgear.fetchUserInfo();
          setGreetingMessage("The current User sub: " + userInfo.sub);
        }
      } finally {
        setIsLoading(false);
      }
    }

    updateGreetingMessage().catch((e) => {
      console.error(e);
    });
  }, [isLoggedIn]);

  const startLogin = useCallback(() => {
    authgear
      .startAuthentication({
        redirectURI: "http://localhost:4000/auth-redirect",
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authentication, user should be redirected to Authgear
        },
        (err) => {
          // failed to start authentication
        }
      );
  }, []);

  return (
    <div>
      <h1>Home Page</h1>
      {isLoading && "Loading"}
      {greetingMessage ? <span>{greetingMessage}</span> : null}
      {!isLoggedIn && (
        <div>
          <button type="button" onClick={startLogin}>
            Login
          </button>
        </div>
      )}
    </div>
  );
};

export default Home;

再次运行应用,用户的用户 ID(sub)应该会打印在 Home 页面上。

步骤 8:添加登出按钮

现在,让我们添加一个仅在用户登录时显示的登出按钮。

在 Home.tsx 中,我们将使用条件渲染元素,只在用户当前已登录时显示登出按钮。

在 Home.tsx 中找到以下这一行:

{!isLoggedIn && (
        <div>
          <button type="button" onClick={startLogin}>
            Login
          </button>
        </div>
)}

在上面这一行之后新起一行,添加以下代码:

{isLoggedIn && (
  <div>
    <button onClick={logout}>Logout</button>
  </div>
)}

然后,添加 logout 回调:

const logout = useCallback(() => {
  authgear
    .logout({
      redirectURI: "http://localhost:4000/",
    })
    .then(
      () => {
        setGreetingMessage('');
      },
      (err) => {
        console.error(err);
      }
  );
}, []);

再次运行应用,现在我们可以通过点击登出按钮来登出。

步骤 9:打开用户设置

Authany 提供了内置界面,供用户设置个人属性和修改安全设置。

使用 openURL 函数打开位于 <your_app_endpoint>/settings 的设置页面

在 Home.tsx 中,为现有元素添加一个条件渲染的链接。

{isLoggedIn && (
  <a target="_blank" rel="noreferrer" onClick={userSetting} href="#">
    User Setting
  </a>
)}

然后添加 userSetting 回调:

import authgear, { Page } from "@authgear/web";
const userSetting = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    e.stopPropagation();
    authgear.open(Page.Settings);
}, []);

最终的 Home.tsx 如下所示:

// src/Home.tsx
import React, { useEffect, useState, useCallback, useContext } from "react";
import { UserContext } from "./context/UserProvider";
import authgear, { Page, PromptOption } from "@authgear/web";

const Home: React.FC = () => {
  const [greetingMessage, setGreetingMessage] = useState<string>("");
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const { isLoggedIn } = useContext(UserContext);

  useEffect(() => {
    async function updateGreetingMessage() {
      setIsLoading(true);
      try {
        if (isLoggedIn) {
          const userInfo = await authgear.fetchUserInfo();
          setGreetingMessage("The current User sub: " + userInfo.sub);
        }
      } finally {
        setIsLoading(false);
      }
    }

    updateGreetingMessage().catch((e) => {
      console.error(e);
    });
  }, [isLoggedIn]);

  const startLogin = useCallback(() => {
    authgear
      .startAuthentication({
        redirectURI: "http://localhost:4000/auth-redirect",
        prompt: PromptOption.Login,
      })
      .then(
        () => {
          // started authorization, user should be redirected to Authgear
        },
        (err) => {
          // failed to start authorization
          console.error(err);
        }
      );
  }, []);

  const logout = useCallback(() => {
    authgear
      .logout({
        redirectURI: "http://localhost:4000/",
      })
      .then(
        () => {
          setGreetingMessage("");
        },
        (err) => {
          console.error(err);
        }
      );
  }, []);

  const userSetting = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    e.stopPropagation();
    authgear.open(Page.Settings);
  }, []);

  return (
    <div>
      {/* eslint-disable-next-line react/forbid-elements */}
      <h1>Home Page</h1>
      {isLoading && "Loading"}
      {greetingMessage ? <span>{greetingMessage}</span> : null}
      {!isLoggedIn && (
        <div>
          <button type="button" onClick={startLogin}>
            Login
          </button>
        </div>
      )}
      {isLoggedIn && (
        <div>
          <button type="button" onClick={logout}>
            Logout
          </button>
          <br />
          <a target="_blank" rel="noreferrer" onClick={userSetting} href="#">
            User Setting
          </a>
        </div>
      )}
    </div>
  );
};

export default Home;

登录后显示用户 ID、指向用户设置的链接和登出按钮

后续步骤:调用 API

要访问后端应用服务器上受限制的资源,HTTP 请求需要在 Authorization 请求头中携带访问令牌。Web SDK 提供了 fetch 函数来自动处理这一过程,您也可以通过 authgear.accessToken 获取令牌。

选项 1:使用 Authany SDK 提供的 fetch 函数

Authany SDK 提供了 fetch 函数,供您调用应用服务器。这个 fetch 函数会在应用请求中自动携带 Authorization 请求头,并自动处理访问令牌的刷新。authgear.fetch 实现了 fetch 规范。

authgear
    .fetch("YOUR_SERVER_URL")
    .then(response => response.json())
    .then(data => console.log(data));

选项 2:将访问令牌添加到 HTTP 请求头

您可以通过 authgear.accessToken 获取访问令牌。每次使用访问令牌之前都调用一次 refreshAccessTokenIfNeeded,该函数只会在令牌过期时才发起网络请求进行刷新。然后将访问令牌加入应用请求的 Authorization 请求头中。

authgear
    .refreshAccessTokenIfNeeded()
    .then(() => {
        // access token is ready to use
        // accessToken can be string or undefined
        // it will be empty if user is not logged in or session is invalid
        const accessToken = authgear.accessToken;

        // include Authorization header in your application request
        const headers = {
            Authorization: `Bearer ${accessToken}`
        };
    });