在React和Express应用中配置新版Google身份验证

本文详细介绍了如何在React.js和Express.js应用中集成新版Google身份验证,包括生成Google客户端ID和密钥、配置前后端认证流程、实现One Tap登录功能以及自定义登录按钮等完整实现步骤。

在React和Express应用中配置新版Google身份验证

主要内容

简化Google身份验证集成

本文提供了在React.js和Express.js应用中集成新版"使用Google登录"按钮的完整指南,重点介绍了简化的身份验证流程,包括允许用户使用个人资料图片选择账户的功能,以及由于旧版Google Sign-In JavaScript库已弃用而必须采用此方法的必要性。

分步实施指南

从在Google控制台生成Google客户端ID和密钥,到设置React Google身份验证和Express环境,文章详细介绍了实现Google身份验证所需的每个步骤,包括处理客户端和服务器端配置,并为流程的每个部分提供代码片段和解释。

最终成果和资源

完成所述步骤后,开发人员将拥有一个利用Google安全登录功能的可运行身份验证系统。文章还链接了服务器和客户端实现的完整源代码,确保读者能够获得成功在项目中实施新版Google身份验证方法所需的所有资源。

生成Google客户端ID和密钥

要设置使用Google登录React,第一步是使用Google Cloud Console生成Google客户端ID和密钥。这些凭据对于在应用中设置安全身份验证至关重要。

按照以下步骤创建新的OAuth 2.0客户端并配置项目以支持Web应用和One Tap登录。

步骤1:导航到Google控制台

首先前往Google控制台。

步骤2:创建新项目

点击顶部导航中的项目下拉菜单,选择"新建项目"。然后点击下面高亮显示的新建项目。

步骤3:输入项目详情

输入项目名称,例如connect-google-auth-2024,然后点击"创建"。

然后导航回项目下拉菜单并选择新创建的项目。

步骤4:配置OAuth同意屏幕

从左菜单点击"API和服务"。您将看到的屏幕应如下所示。

然后点击"OAuth同意屏幕"配置OAuth同意。

选择所需的同意类型,然后点击CREATE。

完成同意屏幕表单,提供应用详情,如应用名称、支持邮箱和徽标。保存设置。

注意:当您准备部署应用时,应将URI1和URI2替换为您要使用的域名,例如https://example.com。

步骤5:创建凭据

转到"凭据"并创建新的OAuth 2.0客户端ID:

应用类型:Web应用

授权重定向URI:添加http://localhost和http://localhost:3000。(部署到生产环境时,将这些替换为您的域名,例如https://example.com。)

步骤6:下载客户端ID和密钥

凭据成功存储后,您可以复制或下载生成的客户端ID和密钥。

设置React应用

首先使用Create React App或等效的现代设置引导您的React.js应用。打开终端,创建项目文件夹,并运行以下命令:

1
npx create-react-app app

对于现代应用,考虑使用Vite等工具以获得更快的构建。安装@react-oauth/google包以利用Google的身份服务SDK:

1
npm install @react-oauth/google

设置Express服务器

在根目录中创建另一个名为server的文件夹。然后打开终端并cd到server:cd server。

之后,创建名为server.js的文件并运行npm init -y以创建package.json文件。接下来安装以下包:

  • Express.js:一个极简的Node.js Web应用框架,为Web和移动应用提供强大的功能集。
  • CORS:一个Node.js包,提供Connect/Express中间件,可用于启用具有各种选项的跨源资源共享。
  • Dotenv:一个Node.js包,从.env文件加载环境变量。
  • Google-auth-library:Google API的Node.js身份验证客户端库。
  • Jsonwebtoken:一个用于Node.js的JSON Web Token实现库。
  • Nodemon:一个用于Node.js应用开发期间的简单监视脚本。

通过运行以下命令安装上述包:

1
npm install express cors dotenv google-auth-library jsonwebtoken nodemon

之后,通过以下方式配置脚本:

1
2
3
4
5
// package.json
"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

您的package.json应如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "connect-google-auth-article",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.2",
    "express": "^4.18.1",
    "google-auth-library": "^8.5.2",
    "jsonwebtoken": "^8.5.1",
    "nodemon": "^2.0.20"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

之后,在server.js中编写以下代码并运行npm run dev以启动服务器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// server.js
const express = require("express");
const app = express();
require("dotenv/config"); // 配置从.env读取
const cors = require("cors");
const { OAuth2Client } = require("google-auth-library");
const jwt = require("jsonwebtoken");

app.use(
  cors({
    origin: ["http://localhost:3000"],
    methods: "GET,POST,PUT,DELETE,OPTIONS",
  })
);
app.use(express.json());

let DB = [];

app.listen("5152", () => console.log("Server running on port 5152"));

准备React应用

现代应用不再需要手动添加Google脚本。相反,从@react-oauth/google包导入GoogleLogin组件以获得更简洁的集成。示例:

1
import { GoogleLogin } from '@react-oauth/google';

我们的index.html文件应如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!-- Google Signup/signin script  -->
    <script src="https://accounts.google.com/gsi/client" async defer></script>
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

接下来,我们将在src文件夹中创建两个名为screens和hooks的文件夹。screens文件夹将包含五个文件:Home.jsx、Landing.jsx、Login.jsx、Signup.jsx和index.js。hooks文件夹将仅包含一个文件:useFetch.jsx。

配置客户端路由

我们将用于客户端路由的包是react-router-dom。打开新终端,cd到app,并运行以下命令:

1
npm install react-router-dom

然后我们可以更新App.js如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// App.js
import React, { useEffect } from "react";
import { useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";

const App = () => {
  const [user, setUser] = useState({});

  return (
    <BrowserRouter>
      <Routes>

      </Routes>
    </BrowserRouter>
  );
};

export default App;

创建登录页面

在此应用中,登录页面是未认证用户唯一可用的页面。它将包含指向注册和登录页面的链接,并如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Landing.jsx
import React from "react";
import { Link } from "react-router-dom";

const Landing = () => {
  return (
    <>
      <header style={{ textAlign: "center" }}>
        <h1>Welcome to my world</h1>
      </header>
      <main style={{ display: "flex", justifyContent: "center", gap: "2rem" }}>
        <Link
          to="/signup"
          style={{
            textDecoration: "none",
            border: "1px solid gray",
            padding: "0.5rem 1rem",
            backgroundColor: "wheat",
            color: "#333",
          }}
        >
          Sign Up
        </Link>
        <Link
          to="/login"
          style={{
            textDecoration: "none",
            border: "1px solid gray",
            padding: "0.5rem 1rem",
            backgroundColor: "whitesmoke",
            color: "#333",
          }}
        >
          Login
        </Link>
      </main>
    </>
  );
};

export default Landing;

让我们分解一下:

  • 组件返回一个由空标签表示的React片段元素。
  • 片段包含两个元素:
    。header返回一个

    并将其文本居中,而main元素返回两个来自react-router-dom的链接并将其居中。

  • 为两个链接提供不同的背景颜色以改善用户体验。

接下来,我们可以打开screens/index.js文件并导出Landing.jsx:

1
2
// index.js
export { default as Landing } from "./Landing";

之后,我们可以将其导入App.js文件,在其中为其配置路由:

1
2
3
4
5
6
7
// App.js
import { Landing } from "./screens";

<Route
  path="/"
  element={user?.email ? <Navigate to="/home" /> : <Landing />}
/>

创建useFetch Hook

React中的hook是一个特殊函数,允许您使用React的功能。要创建hook,打开hooks/useFetch.jsx并添加以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// useFetch.jsx
import { useState } from "react";

const useFetch = (url) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const handleGoogle = async (response) => {
    console.log(response)
  };
  return { loading, error, handleGoogle };
};

export default useFetch;

创建注册页面

打开screens/Signup.jsx文件并添加以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Signup.jsx
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";

// https://developers.google.com/identity/gsi/web/reference/js-reference

const SignUp = () => {
  const { handleGoogle, loading, error } = useFetch(
    "http://localhost:5152/signup"
  );

  useEffect(() => {
    /* global google */
    if (window.google) {
      google.accounts.id.initialize({
        client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        callback: handleGoogle,
      });

      google.accounts.id.renderButton(document.getElementById("signUpDiv"), {
        // type: "standard",
        theme: "filled_black",
        // size: "small",
        text: "continue_with",
        shape: "pill",
      });

      // google.accounts.id.prompt()
    }
  }, [handleGoogle]);

  return (
    <>
      <nav style={{ padding: "2rem" }}>
        <Link to="/">Go Back</Link>
      </nav>
      <header style={{ textAlign: "center" }}>
        <h1>Register to continue</h1>
      </header>
      <main
        style={{
          display: "flex",
          justifyContent: "center",
          flexDirection: "column",
          alignItems: "center",
        }}
      >
        {error && <p style={{ color: "red" }}>{error}</p>}
        {loading ? (
          <div>Loading....</div>
        ) : (
          <div id="signUpDiv" data-text="signup_with"></div>
        )}
      </main>
      <footer></footer>
    </>
  );
};

export default SignUp;

让我们分解一下:

  • 我们从useFetch hook中提取可用的状态和函数。我们还传递我们将调用以处理服务器登录的URL。
  • 在useEffect中,我们检查Google脚本的可用性——由我们在public.index.html文件中放置的脚本处理。
  • 然后我们使用脚本中可用的initialize方法来处理身份验证按钮的功能。
  • 我们还传递一个回调函数,该函数已在useFetch hook中定义。

接下来,我们将使用renderButton方法在屏幕上显示我们的身份验证按钮。我们传递的第一个参数是按钮将嵌入的元素,使用getElementById方法。我们可以传递的下一个参数用于自定义按钮的外观。它具有以下必需设置:

  • type:接受两个值——standard和icon。

此外,它还有可选设置,包括以下内容:

  • theme:按钮主题。可以接受以下之一:filled_blue、outline和filled_black。
  • size:定义按钮的大小。接受large、medium和small。
  • text:定义按钮文本。接受以下之一:signin_with、signup_with、continue_with和sign in。
  • shape:定义按钮的形状。接受rectangular、pill、circle或square。
  • logo_alignment:定义徽标将如何放置在按钮上。可以是left或center。
  • width:定义按钮的宽度。最大宽度为400。
  • locale:用于为文本设置特定语言。

创建登录页面

登录页面类似于注册屏幕。唯一的区别是服务器URL和按钮文本。代码应如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Login.jsx
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";

// https://developers.google.com/identity/gsi/web/reference/js-reference

const Login = () => {
  const { handleGoogle, loading, error } = useFetch(
    "http://localhost:5152/login"
  );

  useEffect(() => {
    /* global google */
    if (window.google) {
      google.accounts.id.initialize({
        client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        callback: handleGoogle,
      });

      google.accounts.id.renderButton(document.getElementById("loginDiv"), {
        // type: "standard",
        theme: "filled_black",
        // size: "small",
        text: "signin_with",
        shape: "pill",
      });

      // google.accounts.id.prompt()
    }
  }, [handleGoogle]);

  return (
    <>
      <nav style={{ padding: "2rem" }}>
        <Link to="/">Go Back</Link>
      </nav>
      <header style={{ textAlign: "center" }}>
        <h1>Login to continue</h1>
      </header>
      <main
        style={{
          display: "flex",
          justifyContent: "center",
          flexDirection: "column",
          alignItems: "center",
        }}
      >
        {error && <p style={{ color: "red" }}>{error}</p>}
        {loading ? <div>Loading....</div> : <div id="loginDiv"></div>}
      </main>
      <footer></footer>
    </>
  );
};

export default Login;

还要在根文件夹中创建.env.local文件并添加以下内容:

1
REACT_APP_GOOGLE_CLIENT_ID=your client id

接下来,我们从screens.index.js文件导出注册和登录页面:

1
2
3
// index.js...
export { default as Login } from "./Login";
export { default as Signup } from "./SignUp";

之后,我们在App.js文件中配置它们的路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// App.js
import { Landing, Login, Signup } from "./screens";

...

<Route
    path="/signup"
    element={user?.email ? <Navigate to="/home" /> : <Signup />}
  />
  <Route
    path="/login"
    element={user?.email ? <Navigate to="/home" /> : <Login />}
  />

google.accounts.id.prompt()用于在用户打开网页后立即自动要求用户登录。它可以放在根文件或登录页面中。此行为称为one-tap登录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
useEffect(() => {
  if (window.google) {
    google.accounts.id.initialize({
      client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
      callback: handleGoogle,
    });

    // Enable One Tap
    google.accounts.id.prompt((notification) => {
      if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
        console.warn("One Tap prompt was not displayed.");
      }
    });
  }
}, [handleGoogle]);

// Token refresh

const refreshAccessToken = async () => {
  try {
    const response = await fetch('http://localhost:5152/refresh-token', {
      method: 'POST',
      credentials: 'include', // includes cookies
    });

    const data = await response.json();
    if (data.token) {
      localStorage.setItem("user", JSON.stringify(data));
    } else {
      throw new Error('Token refresh failed.');
    }
  } catch (error) {
    console.error('Token refresh error:', error);
  }
};

创建自定义登录按钮

有时,默认的Google登录按钮可能与您的品牌不一致。在这种情况下,您可以创建如下自定义按钮:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useGoogleLogin } from '@react-oauth/google';

const CustomGoogleButton = () => {
  const login = useGoogleLogin({
    onSuccess: (response) => console.log(response),
    onError: () => console.error('Login Failed'),
  });

  return (
    <button
      onClick={login}
      style={{
        backgroundColor: '#4285F4',
        color: 'white',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
      }}
    >
      Sign in with Google
    </button>
  );
};

export default CustomGoogleButton;

更新useFetch

Google身份验证返回带有JWT凭据的响应。但是,为了验证其真实性并为用户创建会话,我们将向服务器发出后续调用。我们应该更新hooks/useFetch文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// useFetch.jsx
  const handleGoogle = async (response) => {
    setLoading(true);
    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },

      body: JSON
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计