运行 Node.js Google OAuth2 项目
- 从 github.com/wpcodevo/go… 下载或克隆 Node.js Google OAuth 项目,然后在 IDE 中打开源代码。
- 在根目录下的控制台运行,更改 Git 分支。尽管如此,请随意使用 master 分支。
git checkout google-oauth2-nodejs - 通过运行 或 来安装项目的依赖项。
yarn``yarn install - 复制文件并将复制的文件重命名为 。
example.env``.env - 按照“获取 Google OAuth2 凭证”部分从 Google API 控制台获取 OAuth2 客户端 ID 和密钥。
- 将 Google OAuth2 凭据添加到文件中。
.env - 通过运行 将 Prisma 架构推送到 SQLite 数据库。
npx prisma db push - 运行 以启动 Express HTTP 服务器。
yarn start - 设置 React 应用程序以与 Node.js API 交互。
使用 React.js 应用程序运行 Node.js API
有关如何在 React 应用程序中设置 Google OAuth2 流程的完整指南,请参阅文章“如何在 React.js 中实现 Google OAuth2”。但是,您可以按照以下步骤在几分钟内启动应用程序。
- 从 github.com/wpcodevo/go… 下载或克隆 React Google OAuth2 项目,并在代码编辑器中打开该项目。
- 运行 或 安装必要的依赖项。
yarn``yarn install - 复制文件并将复制的文件重命名为 。
example.env``.env.local - 将 OAuth2 客户端 ID 和客户端密钥添加到文件中。
.env.local - 运行 以启动 Vite 开发服务器。
yarn dev - 从 React 应用程序与 Node.js API 交互。
设置 Node.js 项目
首先,导航到系统上的方便位置并创建项目目录。对于本教程的 seeve,您可以将 project 命名为 。完成后,导航到该文件夹并使用 Yarn 初始化 Node.js 项目。google-oauth2-nodejs
mkdir google-oauth2-nodejs
cd google-oauth2-nodejs
yarn init -y
这将创建一个包含Node.js应用程序初始设置的文件。完成后,运行以下命令来安装项目所需的依赖项。package.json
yarn add @prisma/client axios cookie-parser cors dotenv express jsonwebtoken qs zod
yarn add -D typescript ts-node-dev prisma morgan @types/qs @types/node @types/morgan @types/jsonwebtoken @types/express @types/cors @types/cookie-parser
@prisma/client– 一个自动生成的查询生成器,支持类型安全的数据库访问。axios– 用于浏览器和Node.js的基于 Promise 的 HTTP 客户端。cookie-parser– 用于解析 HTTP 请求 cookie 的中间件。cors– Node.js CORS 中间件。dotenv– 从文件加载环境变量。.envexpress– Node.js 的 Web 框架。jsonwebtoken– JavaScript 项目的 JSON Web 令牌实现。qs– 用于解析和字符串化查询参数的库。zod– 架构验证库。typescript– 一种用于 JavaScript 开发的语言。ts-node-dev– 编译 TypeScript 文件并在所需文件更改时重新启动服务器。prisma– 一个 CLI 工具,允许您从命令行与您的 Prisma 项目进行交互。morgan– 用于 Node.js 的 HTTP 请求中间件记录器
安装完成后,在 IDE 或文本编辑器中打开项目。接下来,在根目录中创建一个文件,并添加以下 TypeScript 配置。tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictPropertyInitialization": false,
"skipLibCheck": true
}
}
现在,让我们通过构建一个基本的 Express.js 服务器来使用简单的 JSON 对象进行响应。为此,请在根目录中创建一个 src 文件夹。在 src 文件夹中,创建一个文件并添加以下代码。app.ts
来源 / app.ts
import express, { Request, Response } from "express";
const app = express();
app.get("/api/healthChecker", (req: Request, res: Response) => {
res.status(200).json({
status: "success",
message: "Implement Google OAuth2 in Node.js",
});
});
const port = 8000;
app.listen(port, () => {
console.log(`✅ Server started on port: ${port}`);
});
在上面,我们创建了一个 Express 服务器,它侦听端口 8000 并有一个路由 ,该路由返回 200 OK 状态代码和一个 JSON 对象。/api/healthChecker
打开文件并添加以下脚本:package.json
package.json
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only src/app.ts",
"db:migrate": "npx prisma migrate dev --name 'users' --create-only",
"db:generate": "npx prisma generate",
"db:push": "npx prisma db push"
}
}
start– 此脚本将启动 Express HTTP 服务器,并在所需文件更改时热重新加载服务器。db:migrate– 此脚本将创建 Prisma 迁移文件而不应用它。db:generate– 此脚本将在 node_modules 文件夹中生成 Prisma 客户端。db:push– 此脚本会将 Prisma 迁移文件推送到数据库。
现在运行以启动 Express 服务器并向 . 在几毫秒内,您应该会看到 JSON 对象。yarn start``http://localhost:8000/api/healthchecker
获取 Google OAuth2 凭证
-
转到 Google Cloud Console console.cloud.google.com/ 并选择现有项目或创建新项目。
如果您尚未创建项目,可以通过单击“NEW PROJECT” 按钮创建一个项目。
-
在下一个屏幕上,输入项目名称并单击 “CREATE” 按钮。
-
在短时间内,将创建项目,并提示您从通知中选择新创建的项目。
-
选择项目后,点击左侧边栏中的 “OAuth 同意屏幕” 菜单,然后在下一个屏幕的 “用户类型” 下选择 “外部”。
然后,单击“创建”按钮。 -
在“OAuth 同意屏幕”选项卡上,输入应用程序信息并向下滚动到“应用程序域”部分。
在“App domain”部分,输入应用程序主页、隐私政策和服务条款 URL。
之后,在 “开发者联系信息” 部分输入您的电子邮件,然后单击 “保存并继续” 按钮。 -
在“范围”选项卡上,单击“添加或删除范围”按钮,然后选择 和 。完成后,滚动到底部并单击“更新”按钮。
userinfo.email``userinfo.profile
-
点击 “Test users” 选项卡上的 “ADD USERS” 按钮。当应用程序处于沙盒模式时,仅允许这些用户使用其 Google 帐户对其进行测试。
提供测试用户的电子邮件,然后单击“ADD”按钮。
单击“保存并继续”按钮以保留更改。在 “Summary” 选项卡上,浏览提供的信息,然后单击 “BACK TO DASHBOARD” 按钮。 -
此时,我们已经提供了同意屏幕所需的信息。现在,让我们创建 OAuth 客户端 ID 和密钥。为此,请单击左侧边栏中的“凭据”菜单,然后单击“创建凭据”按钮。
从可用选项中选择“OAuth 客户端 ID”。
-
在“创建 OAuth 客户端 ID”屏幕上,选择“Web 应用程序”作为应用程序类型,并为客户端 ID 提供名称,
然后输入授权重定向 URI,然后单击底部的“创建”按钮。
http://localhost:8000/api/sessions/oauth/google
-
生成 OAuth 客户端 ID 和密钥后,打开文件并添加它们。
.env
打开文件并添加以下环境变量。不要忘记将 OAuth 客户端 ID 和客户端密钥添加到占位符中。.env
.env
DATABASE_URL="file:./dev.db"
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google
NODE_ENV=development
JWT_SECRET=my_ultra_secure_secret
TOKEN_EXPIRES_IN=60
FRONTEND_ORIGIN=http://localhost:3000
使用 Prisma 设置数据库
现在,让我们设置一个数据库来存储用户的帐户信息。为此,我们将使用 Prisma ORM 来访问和更改 SQLite 数据库中的数据。
随意使用任何 Prisma 支持的数据库。执行 Prisma init 命令初始化工程中的 Prisma。
npx prisma init --datasource-provider sqlite
这将创建一个包含 Prisma 架构文件的新目录。打开文件和以下 Prisma 模型。prisma``prisma/schema.prisma
prisma/schema.prisma
model User {
id String @id @default(uuid())
name String
email String @unique
password String
role String @default("user")
photo String @default("default.png")
verified Boolean @default(false)
provider String @default("local")
createdAt DateTime
updatedAt DateTime @updatedAt
@@map(name: "users")
}
Prisma 模型有两个主要用途:
- 它表示基础数据库中的表
- 用作生成的 Prisma Client 的基础。
运行 Prisma migrate 命令创建 SQL 迁移文件,生成 Prisma Client,并将迁移推送到数据库。
npx prisma migrate dev --name init
现在,让我们创建一个 Prisma 客户端的实例和一个函数来检查数据库连接池是否已成功建立。在 src 目录中创建一个 utils 文件夹。在 src/utils 文件夹中,创建一个文件并添加以下代码片段。prisma.ts
来源/utils/prisma.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
async function connectDB() {
try {
await prisma.$connect();
console.log("🚀 Database connected successfully");
} catch (error) {
console.log(error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
export default connectDB;
创建验证架构
为了防止用户在请求正文中发送垃圾值,让我们创建验证架构和中间件,以根据架构中定义的规则验证请求正文。
在 src 目录中创建一个 schema 文件夹。在 src/schema/ 文件夹中,创建一个文件并添加以下架构定义。user.schema.ts
来源/架构/user.schema.ts
import { object, string, TypeOf } from 'zod';
export const createUserSchema = object({
body: object({
name: string({ required_error: 'Name is required' }),
email: string({ required_error: 'Email is required' }).email(
'Invalid email'
),
password: string({ required_error: 'Password is required' })
.min(8, 'Password must be more than 8 characters')
.max(32, 'Password must be less than 32 characters'),
passwordConfirm: string({ required_error: 'Please confirm your password' }),
}).refine((data) => data.password === data.passwordConfirm, {
path: ['passwordConfirm'],
message: 'Passwords do not match',
}),
});
export const loginUserSchema = object({
body: object({
email: string({ required_error: 'Email is required' }).email(
'Invalid email or password'
),
password: string({ required_error: 'Password is required' }).min(
8,
'Invalid email or password'
),
}),
});
export type CreateUserInput = TypeOf<typeof createUserSchema>['body'];
export type LoginUserInput = TypeOf<typeof loginUserSchema>['body'];
现在,让我们创建一个验证中间件,将其添加到请求中间件管道中,以验证传入的请求正文并将适当的验证错误返回给客户端。
为此,请创建一个文件并添加以下代码:src/middleware/validate.ts
src/middleware/validate.ts
import { NextFunction, Request, Response } from "express";
import { AnyZodObject, ZodError } from "zod";
export const validate =
(schema: AnyZodObject) =>
(req: Request, res: Response, next: NextFunction) => {
try {
schema.parse({
params: req.params,
query: req.query,
body: req.body,
});
next();
} catch (err: any) {
if (err instanceof ZodError) {
return res.status(400).json({
status: "fail",
error: err.errors,
});
}
next(err);
}
};
此路由中间件将接受 Zod 架构作为参数,解析请求正文并根据架构中定义的规则验证字段,如果违反了任何规则,则返回验证错误。
获取 Google OAuth 访问令牌和用户信息
如果你能走到这一步,我为你感到骄傲。现在,是时候使用 OAuth 客户端 ID 和客户端密钥从 Google OAuth2 API 获取访问令牌了。如果是国内用户,会存在代理的问题,这个时候就可以使用代理服务器来请求google服务了。
安装代理插件
yarn add https-proxy-agent
首先,创建一个文件并添加以下导入和常量。src/services/session.service.ts
src/services/session.service.ts
import axios from "axios";
import qs from "qs";
import { HttpsProxyAgent } from 'https-proxy-agent';
const GOOGLE_OAUTH_CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID as unknown as string;
const GOOGLE_OAUTH_CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET as unknown as string;
const GOOGLE_OAUTH_REDIRECT = process.env.GOOGLE_OAUTH_REDIRECT as unknown as string;
创建代理服务
const httpsAgent = new HttpsProxyAgent('你的代理服务器ip');
获取 OAuth 访问令牌
要获取 OAuth 访问令牌,我们将创建一个函数,该函数将使用该软件包向 Google OAuth 令牌端点发出 POST 请求,并使用 OAuth 客户端 ID 和密钥检索访问令牌。axios
src/services/session.service.ts
interface GoogleOauthToken {
access_token: string;
id_token: string;
expires_in: number;
refresh_token: string;
token_type: string;
scope: string;
}
export const getGoogleOauthToken = async ({
code,
}: {
code: string;
}): Promise<GoogleOauthToken> => {
const rootURl = "https://oauth2.googleapis.com/token";
const options = {
code,
client_id: GOOGLE_OAUTH_CLIENT_ID,
client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
redirect_uri: GOOGLE_OAUTH_REDIRECT,
grant_type: "authorization_code",
};
try {
const { data } = await axios.post<GoogleOauthToken>(
rootURl,
qs.stringify(options),
{
httpsAgent,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
return data;
} catch (err: any) {
console.log("Failed to fetch Google Oauth Tokens");
throw new Error(err);
}
};
client_id– 客户端 ID 是我们在 Google Cloud Console 上创建项目时发给我们的唯一标识符。client_secret– Google OAuth2 API 将使用客户端密钥来证明应用程序的身份。redirect_uri– 重定向 URI,也称为回调 URL,是 OAuth2 服务器在授予或拒绝用户访问所请求权限后将用户重定向到的 URL。grant_type– 该代码(也称为授权代码)是 OAuth2 服务器在用户授予对其资源的访问权限时颁发的短期代码。我们将使用此代码从 OAuth2 服务器获取访问令牌。
获取 Google 帐户用户
现在我们已经能够获取访问令牌,让我们创建一个函数来使用访问令牌检索用户的账户信息。
src/services/session.service.ts
interface GoogleUserResult {
id: string;
email: string;
verified_email: boolean;
name: string;
given_name: string;
family_name: string;
picture: string;
locale: string;
}
export async function getGoogleUser({
id_token,
access_token,
}: {
id_token: string;
access_token: string;
}): Promise<GoogleUserResult> {
try {
const { data } = await axios.get<GoogleUserResult>(
`https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=${access_token}`,
{
httpsAgent,
headers: {
Authorization: `Bearer ${id_token}`,
},
}
);
return data;
} catch (err: any) {
console.log(err);
throw Error(err);
}
}
此函数将使用 Axios 向 Google OAuth2 的端点发出 GET 请求,以检索用户的公开个人资料信息。为此,该函数会将访问令牌作为查询参数添加到 URL,并将访问令牌作为 Bearer 添加到 Authorization 标头。/v1/userinfo``id_token
在 Node.js 中实施 Google OAuth2
糟糕,相当多的配置和代码。此时,我们现在可以实现 Node.js API 的身份验证方面了。为此,我们将创建中间件函数,当客户端或前端应用程序向服务器发出请求时,Express 会将请求委托给这些函数。
首先,在 src 目录中创建一个 controllers 文件夹。然后,在该文件夹中创建一个文件并添加以下代码。auth.controller.ts``controllers
src/controllers/auth.controller.ts
import { NextFunction, Request, Response } from "express";
import { CreateUserInput, LoginUserInput } from "../schema/user.schema";
import {
getGoogleOauthToken,
getGoogleUser,
} from "../services/session.service";
import { prisma } from "../utils/prisma";
import jwt from "jsonwebtoken";
export function exclude<User, Key extends keyof User>(
user: User,
keys: Key[]
): Omit<User, Key> {
for (let key of keys) {
delete user[key];
}
return user;
}
该函数将允许我们从 Prisma 返回的记录中省略敏感字段,例如密码。exclude
注册用户路由处理程序
当向端点发出 POST 请求时,Express 路由器将调用此路由中间件函数来注册新用户。/api/auth/register
当 Express 将请求转发到此函数时,它将使用请求正文中提供的数据创建一个新的用户对象,并使用 Prisma 客户端将用户保存到数据库。
如果数据库中已存在具有该电子邮件的用户,则将向客户端发送 409 Conflict 响应。但是,如果操作成功,该函数将在响应中返回 201 状态代码和新创建的用户。
src/controllers/auth.controller.ts
export const registerHandler = async (
req: Request<{}, {}, CreateUserInput>,
res: Response,
next: NextFunction
) => {
try {
const user = await prisma.user.create({
data: {
name: req.body.name,
email: req.body.email,
password: req.body.password,
createdAt: new Date(),
},
});
res.status(201).json({
status: "success",
data: {
user: exclude(user, ["password"]),
},
});
} catch (err: any) {
if (err.code === "P2002") {
return res.status(409).json({
status: "fail",
message: "Email already exist",
});
}
next(err);
}
};
登录用户路由处理程序
当向终端节点发出 POST 请求时,将调用此中间件函数以将用户登录到 API。当请求到达此路由处理程序时,它将使用 Prisma 客户端检查数据库中是否存在具有该电子邮件的用户。/api/auth/login
如果存在用户,将生成 JSON Web 令牌,并将其作为仅限 HTTP 的 Cookie 发送到客户端。注意: 为了简化项目,我们将跳过其他身份验证方法,如密码哈希、会话存储等。
src/controllers/auth.controller.ts
export const loginHandler = async (
req: Request<{}, {}, LoginUserInput>,
res: Response,
next: NextFunction
) => {
try {
const user = await prisma.user.findUnique({
where: { email: req.body.email },
});
if (!user) {
return res.status(401).json({
status: "fail",
message: "Invalid email or password",
});
}
if (user.provider === "Google") {
return res.status(401).json({
status: "fail",
message: `Use ${user.provider} OAuth2 instead`,
});
}
const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
expiresIn: `${TOKEN_EXPIRES_IN}m`,
});
res.cookie("token", token, {
expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
});
res.status(200).json({
status: "success",
});
} catch (err: any) {
next(err);
}
};
注销用户路由处理程序
现在我们已经能够注册和登录用户,让我们创建一个 Express 将用于注销用户的路由函数。为此,我们将发送一个过期的 Cookie 来删除用户浏览器或 API 客户端中的现有 Cookie。
src/controllers/auth.controller.ts
export const logoutHandler = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
res.cookie("token", "", { maxAge: -1 });
res.status(200).json({ status: "success" });
} catch (err: any) {
next(err);
}
};
使用 Google OAuth2 路由处理程序进行身份验证
src/controllers/auth.controller.ts
export const googleOauthHandler = async (req: Request, res: Response) => {
const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;
try {
const code = req.query.code as string;
const pathUrl = (req.query.state as string) || "/";
if (!code) {
return res.status(401).json({
status: "fail",
message: "Authorization code not provided!",
});
}
const { id_token, access_token } = await getGoogleOauthToken({ code });
const { name, verified_email, email, picture } = await getGoogleUser({
id_token,
access_token,
});
if (!verified_email) {
return res.status(403).json({
status: "fail",
message: "Google account not verified",
});
}
const user = await prisma.user.upsert({
where: { email },
create: {
createdAt: new Date(),
name,
email,
photo: picture,
password: "",
verified: true,
provider: "Google",
},
update: { name, email, photo: picture, provider: "Google" },
});
if (!user) return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
expiresIn: `${TOKEN_EXPIRES_IN}m`,
});
res.cookie("token", token, {
expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
});
res.redirect(`${FRONTEND_ORIGIN}${pathUrl}`);
} catch (err: any) {
console.log("Failed to authorize Google User", err);
return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
}
};
完成路由处理程序
src/controllers/auth.controller.ts
import { NextFunction, Request, Response } from "express";
import { CreateUserInput, LoginUserInput } from "../schema/user.schema";
import {
getGoogleOauthToken,
getGoogleUser,
} from "../services/session.service";
import { prisma } from "../utils/prisma";
import jwt from "jsonwebtoken";
export function exclude<User, Key extends keyof User>(
user: User,
keys: Key[]
): Omit<User, Key> {
for (let key of keys) {
delete user[key];
}
return user;
}
export const registerHandler = async (
req: Request<{}, {}, CreateUserInput>,
res: Response,
next: NextFunction
) => {
try {
const user = await prisma.user.create({
data: {
name: req.body.name,
email: req.body.email,
password: req.body.password,
createdAt: new Date(),
},
});
res.status(201).json({
status: "success",
data: {
user: exclude(user, ["password"]),
},
});
} catch (err: any) {
if (err.code === "P2002") {
return res.status(409).json({
status: "fail",
message: "Email already exist",
});
}
next(err);
}
};
export const loginHandler = async (
req: Request<{}, {}, LoginUserInput>,
res: Response,
next: NextFunction
) => {
try {
const user = await prisma.user.findUnique({
where: { email: req.body.email },
});
if (!user) {
return res.status(401).json({
status: "fail",
message: "Invalid email or password",
});
}
if (user.provider === "Google") {
return res.status(401).json({
status: "fail",
message: `Use ${user.provider} OAuth2 instead`,
});
}
const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
expiresIn: `${TOKEN_EXPIRES_IN}m`,
});
res.cookie("token", token, {
expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
});
res.status(200).json({
status: "success",
});
} catch (err: any) {
next(err);
}
};
export const logoutHandler = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
res.cookie("token", "", { maxAge: -1 });
res.status(200).json({ status: "success" });
} catch (err: any) {
next(err);
}
};
export const googleOauthHandler = async (req: Request, res: Response) => {
const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;
try {
const code = req.query.code as string;
const pathUrl = (req.query.state as string) || "/";
if (!code) {
return res.status(401).json({
status: "fail",
message: "Authorization code not provided!",
});
}
const { id_token, access_token } = await getGoogleOauthToken({ code });
const { name, verified_email, email, picture } = await getGoogleUser({
id_token,
access_token,
});
if (!verified_email) {
return res.status(403).json({
status: "fail",
message: "Google account not verified",
});
}
const user = await prisma.user.upsert({
where: { email },
create: {
createdAt: new Date(),
name,
email,
photo: picture,
password: "",
verified: true,
provider: "Google",
},
update: { name, email, photo: picture, provider: "Google" },
});
if (!user) return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
const TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN as unknown as number;
const TOKEN_SECRET = process.env.JWT_SECRET as unknown as string;
const token = jwt.sign({ sub: user.id }, TOKEN_SECRET, {
expiresIn: `${TOKEN_EXPIRES_IN}m`,
});
res.cookie("token", token, {
expires: new Date(Date.now() + TOKEN_EXPIRES_IN * 60 * 1000),
});
res.redirect(`${FRONTEND_ORIGIN}${pathUrl}`);
} catch (err: any) {
console.log("Failed to authorize Google User", err);
return res.redirect(`${FRONTEND_ORIGIN}/oauth/error`);
}
};
创建用户路由处理程序
让我们创建一个路由处理程序,当向终端节点发出 GET 请求时,将调用该处理程序以返回经过身份验证的用户的信息。此路由中间件将受到保护,只有具有有效 JWT 的用户才能访问它。/api/users/me
src/controllers/user.controller.ts
import { NextFunction, Request, Response } from "express";
export const getMeHandler = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = res.locals.user;
res.status(200).json({
status: "success",
data: {
user,
},
});
} catch (err: any) {
next(err);
}
};
创建身份验证守卫
在这里,您将创建一个 Express 中间件函数,该函数将通过检查请求标头或 Cookies 对象中的有效令牌来验证用户。
身份验证中间件
中间件函数将检查请求中是否存在 “Authorization” 标头,该标头应包含 JSON Web 令牌 (JWT)。如果标头不存在,它将检查 Cookies 对象,如果请求中未包含令牌,则将向客户端发送 401 Unauthorized 响应。
但是,如果令牌存在于 Authorization 标头或 Cookies 对象中,则该函数将通过调用库提供的方法来验证令牌。jwt.verify()``jsonwebtoken
src/middleware/deserializeUser.ts
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { exclude } from "../controllers/auth.controller";
import { prisma } from "../utils/prisma";
export const deserializeUser = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
token = req.headers.authorization.split(" ")[1];
} else if (req.cookies.token) {
token = req.cookies.token;
}
if (!token) {
return res.status(401).json({
status: "fail",
message: "You are not logged in",
});
}
const JWT_SECRET = process.env.JWT_SECRET as unknown as string;
const decoded = jwt.verify(token, JWT_SECRET);
if (!decoded) {
return res.status(401).json({
status: "fail",
message: "Invalid token or user doesn't exist",
});
}
const user = await prisma.user.findUnique({
where: { id: String(decoded.sub) },
});
if (!user) {
return res.status(401).json({
status: "fail",
message: "User with that token no longer exist",
});
}
res.locals.user = exclude(user, ["password"]);
next();
} catch (err: any) {
next(err);
}
};
如果令牌有效,则将提取有效负载,并进行查询以检查属于该令牌的用户是否仍存在于数据库中。如果用户存在,则数据库返回的记录将作为 response 对象添加,并将请求传递给下一个中间件。res.locals.user
需要用户中间件
然后,此中间件将检查响应以查看对象是否具有用户记录。如果未定义该属性,则会向客户端发送 401 Unauthorized 响应。res.locals``res.locals.user
src/middleware/requireUser.ts
import { NextFunction, Request, Response } from "express";
export const requireUser = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = res.locals.user;
if (!user) {
return res.status(401).json({
status: "fail",
message: "Invalid token or session has expired",
});
}
next();
} catch (err: any) {
next(err);
}
};
创建 API 路由
现在让我们创建 Express 路由器来调用路由中间件功能。第一个路由器将具有以下端点:
/register– 当向 发出 POST 请求时,此路由将调用函数。registerHandler``/api/auth/register/login– 当向终端节点发出 POST 请求时,此路由将触发函数。loginHandler``/api/auth/login/logout– 当向终端节点发出 GET 请求时,此路由将触发函数。logoutHandler``/api/auth/logout
在 src 目录中创建一个 routes 文件夹。在 src/routes/ 文件夹中,创建一个文件并添加以下代码。auth.route.ts
src/routes/auth.route.ts
import express from "express";
import {
loginHandler,
logoutHandler,
registerHandler,
} from "../controllers/auth.controller";
import { deserializeUser } from "../middleware/deserializeUser";
import { requireUser } from "../middleware/requireUser";
import { validate } from "../middleware/validate";
import { createUserSchema, loginUserSchema } from "../schema/user.schema";
const router = express.Router();
router.post("/register", validate(createUserSchema), registerHandler);
router.post("/login", validate(loginUserSchema), loginHandler);
router.get("/logout", deserializeUser, requireUser, logoutHandler);
export default router;
第二个路由器将只有一个端点,当 Oauth2 服务器将用户重定向到 Express 服务器时,将触发该端点。因此,创建一个文件并添加以下代码。/oauth/google``session.route.ts
src/routes/session.route.ts
import express from "express";
import { googleOauthHandler } from "../controllers/auth.controller";
const router = express.Router();
router.get("/oauth/google", googleOauthHandler);
export default router;
最后一个路由器还将有一个 endpoint,当向 endpoint 发出 GET 请求时,将触发该 endpoint。/me``/api/users/me
src/routes/user.route.ts
import express from "express";
import { getMeHandler } from "../controllers/user.controller";
import { deserializeUser } from "../middleware/deserializeUser";
import { requireUser } from "../middleware/requireUser";
const router = express.Router();
router.use(deserializeUser, requireUser);
router.get("/me", getMeHandler);
export default router;
设置 CORS 并注册 API 路由器
最后,让我们将路由器添加到 Express 应用程序,并将服务器配置为接受来自特定来源的跨域请求。为此,请打开该文件,并将其内容替换为以下代码。src/app.ts
来源 / app.ts
require("dotenv").config();
import path from "path";
import express, { NextFunction, Request, Response } from "express";
import morgan from "morgan";
import cors from "cors";
import cookieParser from "cookie-parser";
import userRouter from "./routes/user.route";
import authRouter from "./routes/auth.route";
import sessionRouter from "./routes/session.route";
import connectDB from "./utils/prisma";
const app = express();
app.use(express.json({ limit: "10kb" }));
app.use(cookieParser());
if (process.env.NODE_ENV === "development") app.use(morgan("dev"));
app.use("/api/images", express.static(path.join(__dirname, "../public")));
const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN as unknown as string;
app.use(
cors({
credentials: true,
origin: [FRONTEND_ORIGIN],
})
);
app.use("/api/users", userRouter);
app.use("/api/auth", authRouter);
app.use("/api/sessions", sessionRouter);
app.get("/api/healthChecker", (req: Request, res: Response) => {
res.status(200).json({
status: "success",
message: "Implement OAuth in Node.js",
});
});
// UnKnown Routes
app.all("*", (req: Request, res: Response, next: NextFunction) => {
const err = new Error(`Route ${req.originalUrl} not found`) as any;
err.statusCode = 404;
next(err);
});
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
err.status = err.status || "error";
err.statusCode = err.statusCode || 500;
res.status(err.statusCode).json({
status: err.status,
message: err.message,
});
});
const port = 8000;
app.listen(port, () => {
console.log(`✅ Server started on port: ${port}`);
connectDB();
});
现在,您可以通过运行 来再次启动 Express HTTP 服务器。yarn start
结论
我们完成了!您可以在 GitHub 上找到 Node.js Google OAuth2 项目的源代码。
在本文中,我们使用 TypeScript 和 Node.js 从头开始实现了 Google OAuth 流。该 API 具有所有必需的功能,例如,注册用户、将用户登录到 API、使用 Google OAuth 进行身份验证以及将用户从应用程序中注销。