在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
|