Eswlnk Blog Eswlnk Blog
  • 资源
    • 精彩视频
    • 破解专区
      • WHMCS
      • WordPress主题
      • WordPress插件
    • 其他分享
    • 极惠VPS
    • PDF资源
  • 关于我
    • 论文阅读
    • 关于本站
    • 通知
    • 左邻右舍
    • 玩物志趣
    • 日志
    • 专题
  • 热议话题
    • 游戏资讯
  • 红黑
    • 渗透分析
    • 攻防对抗
    • 代码发布
  • 自主研发
    • 知识库
    • 插件
      • ToolBox
      • HotSpot AI 热点创作
    • 区块
    • 快乐屋
    • 卡密
  • 乱步
    • 文章榜单
    • 热门标签
  • 问答中心反馈
  • 注册
  • 登录
首页 › 其他分享 › NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录

NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录

Eswlnk的头像
Eswlnk
2024-01-30 23:41:57
NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录-Eswlnk Blog
智能摘要 AI
本文介绍了使用NestJS集成Google登录的功能实现。首先,通过`passport-google-oauth20`库配置Google登录,并设置必要的环境变量(如`GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET`和`GOOGLE_REDIRECT_URL`)。接着,创建了`GoogleStrategy`类,以实现OAuth2认证流程。为了满足特定需求(如确保用户表中只有一个管理员),还定制了`GoogleGuard`,通过该守卫可以在用户登录时验证并更新数据库中的用户信息。 随后,将Google登录功能注入到全局模块中,并在特定控制器中使用`@UseGuards`装饰器指定GoogleGuard,以限制某些API仅支持Google登录访问。最后,为了解决前后端分离场景下的用户体验问题,作者建议前端定义一个重定向处理路由,并在Google登录回调中携带参数,通过Axios向后端发起请求以获取token。 总结来说,本文详细讲解了如何利用

前言

实现谷歌登录功能是许多网络应用必备的一项功能,而 Nestjs 利用 passport-google-oauth20 这个库能够轻松实现这一目标。在网络世界中,谷歌登录已成为最为著名的第三方登录方式之一,几乎所有的 Web 应用都会提供谷歌登录的选项。

在之前的文章中,我们已经学习了如何使用 passport 的 jwt 策略来实现本地的 jwt 鉴权登录处理。Passport 本身就是一个实现了策略模式的库,通过调用不同的策略可以实现不同的功能。其中,谷歌登录就是其中之一。这大大简化了我们的开发过程。

需要注意的是,谷歌登录对网络的要求非常高。在我们国内的网络环境中,我尝试使用了 Windows 下的 Clash 进行网络代理,即使设置了全局规则,但 Nestjs 依然无法访问到谷歌的服务,导致出现了报错信息:”Failed to obtain access token”。最终,我通过路由器上的 OpenClash 解决了这个问题。如果你没有这样的设备,也可以通过手机热点的方式来解决,即手机开启网络代理后,通过连接手机热点来使电脑也能够访问谷歌服务。当然,还可能存在其他的解决办法,欢迎在评论区分享你的经验。

接下来,我们将开始具体的教程。

配置数据库表字段

谷歌登录后,我们可以获取到谷歌账号的一些信息,比如固定不变的id字段,以及用户的谷歌邮箱字段,当然我们还可以拿到用户的名字和昵称,但是这些不是很重要,最有用的就是id和email了,为此我们在用户的表结构上,加上这两个字段。

prisma的User配置:

schema.prisma

// 用户表
model User {
  id          Int        @id @default(autoincrement()) @db.UnsignedInt
  email       String     @unique @db.VarChar(64)
  password    String     @db.VarChar(255)
  nickname    String     @db.VarChar(64)
  avatar      String?    @db.VarChar(255)
  googleId    String?    @db.VarChar(64)
  googleEmail String?    @db.VarChar(64)
}

由于用户不是只能谷歌登录,所以这两个字段是可选。

配置完毕后我们运行migrate命令:

dotenv -e .env.development -- npx prisma migrate dev

指定使用.env.development环境变量文件。

然后输入本次改动内容:add google login,当然这个随便填就是了,看自己想法。

然后prisma就会将mysql数据库与你配置的表结构进行同步处理,对应的字段就会有了。

安装插件

pnpm i passport-google-oauth20
pnpm i @types/passport-google-oauth20 -D

注意该插件的文档和实际使用出入有点大,所以仅参考。

这里就不重复安装passport和@nestjs/passport依赖了,可以先去看一下之前的passport的文章。

谷歌开发者创建凭据

打开地址:https://console.cloud.google.com

创建一个新项目,注意是新项目。

打开新建的项目,点击API与服务,或者不用点击。

找到左侧的凭据,点击顶部创建凭据,选择 OAuth 客户端ID

NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录-Eswlnk Blog

应用类型选择web引用,自己取个名称,添加已获授权的 JavaScript 来源的链接地址,实际上就是你需要使用谷歌登录的域名,比如我的域名是www.ceshi.com,那么我就填:

https://www.ceshi.com

如果你是本地测试用

http://localhost

不能使用ip地址就很遗憾的,不过也有办法,比如自定义DNS跳转,这个自己去研究吧。

接着添加已获授权的重定向 URI,这个就是当用户在网页点击并谷歌登录完成后,重定向回来的连接,比如比较常见的:

https://www.ceshi.com/auth/google/callback

点击创建,如果不对,后续还可以重新编辑。

谷歌登录的流程

用户访问Nestjs中控制器定义的Get请求地址,通过passport的策略,会重定向到谷歌登录页面,用户进行登录后,谷歌比对在已获授权的重定向 URI的配置中的网址,与Nestjs中策略传递的callbackURL参数是否有相同的,有的话就重定向到这个参数地址,并在url后面携带对应的query参数。

我们再将query参数传递到另一个api地址上,通过passport的策略解析,得到谷歌用户信息,然后就可以自己自定义操作了,比如关联用户信息之类的。

如果已获授权的重定向 URI的配置中的网址,与Nestjs中策略传递的callbackURL参数没有相同的,就报错。

实现谷歌登录策略

创建文件:google.strategy.ts

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-google-oauth20";
import type { VerifyCallback } from "passport-google-oauth20";

export const GOOGLE_STRATEGY = "google";

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, GOOGLE_STRATEGY) {
    constructor(private readonly config: ConfigService) {
        super({
            clientID: config.get("GOOGLE_CLIENT_ID"),
            clientSecret: config.get("GOOGLE_CLIENT_SECRET"),
            callbackURL: config.get("GOOGLE_REDIRECT_URL"),
            scope: ["email", "profile"]
        });
    }

    async validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise<any> {
        done(null, profile);
    }
}

我们创建了一个常量用于标识该策略的名称,这个后续通过passport的守卫函数调用的时候需要用到。

然后实现了两个方法,一个是构造函数constructor,它通过依赖注入获取到config服务,通过config服务获取到环境变量中配置的数据。

  • clientID 客户端id
  • clientSecret 客户端密匙
  • callbackURL 登录成功后的重定向链接,重定向回来会携带query参数
  • scope 需要的权限范围

其中clientID和clientSecret在创建的OAuth里面。

NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录-Eswlnk Blog

自己把他存到环境变量中去,或者写死也行。

scope这个配置就有点难找了,你可以完全照搬,不过文档中也有说:

文档地址:OAuth 2 范围

我们找到Google OAuth2 API v2这个选项:

NestJS 教程 | 使用 Passport-Google-OAuth20 实现谷歌登录-Eswlnk Blog

红线标出来的就是对应的配置字段。

我们只需要"email", "profile"就够了。

app模块注入策略

在全局注入服务。

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { GoogleStrategy } from "@/common/passport";


const isDev = process.env.NODE_ENV === "development";

@Module({
    imports: [
        ConfigModule.forRoot({
            isGlobal: true,
            envFilePath: [isDev ? ".env.development" : ".env.production"]
        }),
    ],
    controllers: [AppController],
    providers: [
        GoogleStrategy,
        AppService,
    ]
})
export class AppModule {}

局部使用

由于谷歌登录是一个部分api使用的功能,此时我们就不需要将其作为一个全局守卫来使用了。

找到需要使用的控制器文件,如下使用:

import { Controller, Get, UseGuards, Req } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthGuard } from "@nestjs/passport";
import { GOOGLE_STRATEGY } from "@/common/passport";
import type { GoogleLoginData } from "@/common/types";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    /** Google登录 */
    @Get("google")
    @UseGuards(AuthGuard(GOOGLE_STRATEGY))
    googleLogin() {
        /* TODO document why this method 'googleLogin' is empty */
    }

    /** Google登录回调 */
    @Get("google/callback")
    @UseGuards(AuthGuard(GOOGLE_STRATEGY))
    googleCallback(@Req() req) {
        const user: GoogleLoginData = req.user;
        return this.authService.googleLogin(user);
    }
}

引入的UseGuards和AuthGuard用于声明局部守卫,GOOGLE_STRATEGY则是策略的名字,上面也有说。

我们先通过浏览器请求get地址:http://localhost:3000/auth/google,此时重定向到谷歌登录。

谷歌登录后又重定向回来:http://localhost:3000/auth/google/callback,此时链接上存在对应的参数,经过守卫处理后,会在req对象上赋值user属性,这个user就是对应的谷歌用户信息了。

然后我调用了authService.googleLogin,将获取到的谷歌用户信息传入,然后服务去生成token。

GoogleLoginData是我自定义的类型声明,大家自己根据实际返回的内容定义。

到这里基本上就完成了,但是我在给大伙分享点自定义处理。

自定义谷歌登录守卫

由于我设计的数据库中,只会存在一个账号,就是管理员账号,所以谷歌登录后我希望它直接就关联已存在的用户,然后我希望在调用谷歌登录的时候会有一个校验操作,如果不存在一个用户,或者用户量过多,都进行报错处理。

但是这些功能,策略本身病没有提供,所以我们可以像之前jwt鉴权一样,自己继承并自定义。

创建守卫:google.guard.ts

import { GOOGLE_STRATEGY } from "@/common/passport";
import { PrismaService } from "@/modules/prisma/prisma.service";
import { BadRequestException, ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import type { User } from "@prisma/client";

@Injectable()
export class GoogleGuard extends AuthGuard(GOOGLE_STRATEGY) {
    /** 数据库用户数据 */
    private dbUser: User = {
        id: 0,
        email: "",
        password: "",
        nickname: "",
        avatar: "",
        googleId: "",
        googleEmail: "",
        createdAt: undefined,
        updatedAt: undefined,
        deletedAt: undefined
    };

    constructor(private readonly prisma: PrismaService) {
        super();
    }

    async canActivate(context: ExecutionContext) {
        // 查询是否存在一个用户
        const findCount = await this.prisma.user.count();
        if (!Number.isFinite(findCount) || findCount <= 0) {
            throw new BadRequestException("用户不存在或者用户数量不在合法范围内!");
        }
        if (findCount > 1) {
            throw new BadRequestException("用户只能存在一位,请检查用户表用户是否超出!");
        }
        // 获取用户数据
        this.dbUser = await this.prisma.user.findFirst();

        return (await super.canActivate(context)) as boolean;
    }

    handleRequest<TUser = any>(err: any, user: any, info: any, context: ExecutionContext, status?: any): TUser {
        const data = super.handleRequest(err, user, info, context, status);
        data.dbUser = this.dbUser;
        return data;
    }
}

在canActivate中查询用户表,如果数量不是1个就进行报错处理。

如果确实是一个,我们将用户信息保存起来,因为handleRequest方法只能是同步的,所以没法在这里进行prisma的异步查询。

canActivate会在用户访问:http://localhost:3000/auth/google时运行,handleRequest则是在访问http://localhost:3000/auth/google/callback时运行。

为了方便,我将查询的数据库用户信息也存放在data中了,这个data就是req.user的值。

然后我们再去控制器使用:

import { Controller, Get, UseGuards, Req } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { GoogleGuard } from "@/common/guards";
import type { GoogleLoginData } from "@/common/types";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    /** Google登录 */
    @Get("google")
    @UseGuards(GoogleGuard)
    googleLogin() {
        /* TODO document why this method 'googleLogin' is empty */
    }

    /** Google登录回调 */
    @Get("google/callback")
    @UseGuards(GoogleGuard)
    googleCallback(@Req() req) {
        const user: GoogleLoginData = req.user;
        return this.authService.googleLogin(user);
    }
}

这种方式相对来说使用简洁一些。

服务具体如何生成token,这个之前文章就有分享,就不重复赘述了,直接放个简略代码:

import { Injectable } from "@nestjs/common";
import { PrismaService } from "@/modules/prisma/prisma.service";
import type { GoogleLoginData } from "@/common/types";

@Injectable()
export class AuthService {
    constructor(
        private readonly prisma: PrismaService,
    ) {}


    /** 谷歌登录 */
    async googleLogin(data: GoogleLoginData) {
        const { id, emails } = data;
        let { dbUser } = data;

        // 更新用户数据
        if (!dbUser.googleId && !dbUser.googleEmail) {
            dbUser = await this.prisma.user.update({
                where: {
                    id: dbUser.id
                },
                data: {
                    googleId: id,
                    googleEmail: emails[0].value
                }
            });
        }

        return this.generateDoubleToken(dbUser);
    }

}

实战扩展

在真正的项目中,我们谷歌登录成功肯定不能直接重定向到Google登录回调的api地址,毕竟是前后端分离了,后端只会返回api数据,不会去种cookie这些了,生成的token数据也需要前端自己取出后存在本地缓存中。

所以正确的做法就是前端定义一个用来重定向处理的路由,比如前端路由:https://xxxx.com/google,在已获授权的重定向 URI中将该url地址添加上。

后端的callbackURL也配置成该地址。

此时用户登录后重定向到的还是前端spa页面,前端就可以从链接上取出query参数,然后通过axios的get请求,请求后端定义的Google登录回调的api地址,效果和谷歌直接重定向到该地址是一样的,甚至我们还能携带额外的参数,并不会影响到谷歌登录策略。

然后api再返回token,前端拿到token进行处理。这样用户体验才是正常的。

本站默认网盘访问密码:1166
本站默认网盘访问密码:1166
声明:本站原创文章文字版权归本站所有,转载务必注明作者和出处;本站转载文章仅仅代表原作者观点,不代表本站立场,图文版权归原作者所有。如有侵权,请联系我们删除。
nestjsoauth安全登录谷歌
1
0
Eswlnk的头像
Eswlnk
一个有点倒霉的研究牲站长
赞赏
「技术教程」OpenWrt配置OpenClash教程及解决启动问题
上一篇
「技术教程」极路由4增强版HC5962固件刷写与优化教程 | 刷潘多拉固件教程
下一篇

评论 (0)

请登录以参与评论
现在登录
    发表评论

猜你喜欢

  • 「漏洞资讯」CVE-2025-12914:宝塔面板曝出注入漏洞
  • 「亲测有效」Google Gemini 学生优惠:解决身份验证和支付卡验证
  • 来自谷歌27岁的生日涂鸦
  • 解决国际版EdgeOne绑卡和手机验证问题
  • 小工具开发之EdgeOne免费计划兑换工具
Eswlnk的头像

Eswlnk

一个有点倒霉的研究牲站长
1108
文章
319
评论
679
获赞

随便看看

「其他分享」Windows KMS 激活密钥对照表
2022-08-15 10:06:26
一证通查查询名下互联网账户
2022-08-09 10:52:49
如何将震源球绘制在谷歌地球上
2022-06-17 17:04:00

文章目录

专题展示

WordPress53

工程实践37

热门标签

360 AI API CDN java linux Nginx PDF PHP python SEO Windows WordPress 云服务器 云服务器知识 代码 免费 安全 安卓 工具 开发日志 微信 微软 手机 插件 攻防 攻防对抗 教程 日志 渗透分析 源码 漏洞 电脑 破解 系统 编程 网站优化 网络 网络安全 脚本 苹果 谷歌 软件 运维 逆向
  • 首页
  • 知识库
  • 地图
Copyright © 2023-2025 Eswlnk Blog. Designed by XiaoWu.
本站CDN由 壹盾安全 提供高防CDN安全防护服务
蜀ICP备20002650号-10
页面生成用时 1.036 秒   |  SQL查询 26 次
本站勉强运行:
友情链接: Eswlnk Blog 网站渗透 倦意博客 特资啦!个人资源分享站 祭夜博客 iBAAO壹宝头条
  • WordPress142
  • 网络安全64
  • 漏洞52
  • 软件52
  • 安全48
现在登录
  • 资源
    • 精彩视频
    • 破解专区
      • WHMCS
      • WordPress主题
      • WordPress插件
    • 其他分享
    • 极惠VPS
    • PDF资源
  • 关于我
    • 论文阅读
    • 关于本站
    • 通知
    • 左邻右舍
    • 玩物志趣
    • 日志
    • 专题
  • 热议话题
    • 游戏资讯
  • 红黑
    • 渗透分析
    • 攻防对抗
    • 代码发布
  • 自主研发
    • 知识库
    • 插件
      • ToolBox
      • HotSpot AI 热点创作
    • 区块
    • 快乐屋
    • 卡密
  • 乱步
    • 文章榜单
    • 热门标签
  • 问答中心反馈