本指南承接添加 Blacklist,添加 oauth 功能。
oauth 功能让用户可以通过 Google、GitHub 这样的 OAuth 提供商登录。和其他登录方式一样,OAuth 登录使用相同的用户模型和 JWT 流程。
本指南使用 email 作为账号标识符。如果邮箱已存在,就登录该用户;如果不存在,就先创建一个新用户。
完成本指南后,你的应用将支持:
POST /auth/oauth/googlePOST /auth/oauth/github
- 已完成添加 Blacklist的项目
- Google 和 GitHub 的 OAuth client ID 与 client secret
添加 OAuth 功能
Section titled “添加 OAuth 功能”添加功能
运行 CLI 命令:
brkpt auth add oauth --driver google,githubCLI 会添加一个新的 features/oauth/ 文件夹,包含所选驱动的文件。
如果某个驱动需要额外依赖,请按照 CLI 显示的依赖提示操作。例如,Google 驱动依赖 google-auth-library。
配置提供商凭证
把提供商的 client ID 和 client secret 添加到已有的 .env 文件中:
GOOGLE_CLIENT_ID="your-google-client-id"GOOGLE_CLIENT_SECRET="your-google-client-secret"GITHUB_CLIENT_ID="your-github-client-id"GITHUB_CLIENT_SECRET="your-github-client-secret"配置 BrkptAuthModule
在 BrkptAuthModule.forRootAsync 中添加 oauth 配置:
BrkptAuthModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ jwt: { // ... }, oauth: { google: { clientId: config.getOrThrow('GOOGLE_CLIENT_ID'), clientSecret: config.getOrThrow('GOOGLE_CLIENT_SECRET'), }, github: { clientId: config.getOrThrow('GITHUB_CLIENT_ID'), clientSecret: config.getOrThrow('GITHUB_CLIENT_SECRET'), }, }, }),}),定义并校验 DTO
oauth 功能默认生成空的 DTO。请求体会被传给由 :provider 路由参数选中的驱动。
在本指南中,/auth/oauth/google 使用 GoogleOAuthDriver,其 verify 方法读取 idToken;/auth/oauth/github 使用 GithubOAuthDriver,其 verify 方法读取 code。
把这些字段加入 DTO:
import { IsOptional, IsString } from 'class-validator';
export class OAuthDto { @IsOptional() @IsString() idToken?: string;
@IsOptional() @IsString() code?: string;}字段名必须和所选驱动在其 verify 方法中读取的字段一致。由于 Google 和 GitHub 共用这个 DTO,这里两个字段都是可选的,具体哪个是必填由所选驱动决定。
定义用户画像类型
不同提供商返回的用户数据结构不同。在 adapters/types.ts 中添加 UserProfile,让适配器在查找或创建用户之前先把这些数据归一化:
export interface UserProfile { name: string; email: string;}实现 OAuthAdapter
OAuth 适配器把提供商数据转换成 UserProfile,然后查找或创建用户。
创建 oauth.adapter.ts,实现基于 Prisma 的 OAuth 适配器:
文件夹src/
文件夹brkpt-auth/
文件夹adapters/
- oauth.adapter.ts
import { BadRequestException, Injectable } from '@nestjs/common';import { TokenPayload } from 'google-auth-library';
import { User } from '../../../generated/prisma/client';import { PrismaService } from '../../prisma/prisma.service';import { OAuthPort } from '../features/oauth/oauth.port';import { UserProfile } from './types';
interface GoogleUser extends TokenPayload { name: string; email: string;}
interface GitHubUser { id: number; login: string; name: string | null; email: string | null;}
@Injectable()export class OAuthAdapter implements OAuthPort<User, UserProfile> { constructor(private readonly prisma: PrismaService) {}
mapRawToProfile(provider: string, raw: unknown): UserProfile | undefined { switch (provider) { case 'google': { const r = raw as GoogleUser; if (!r.email) { throw new BadRequestException( 'Google profile does not include an email address', ); } return { name: r.name, email: r.email }; } case 'github': { const r = raw as GitHubUser; if (!r.email) { throw new BadRequestException( 'GitHub profile does not include an email address', ); } return { name: r.name ?? r.login, email: r.email ?? '' }; } } }
async findOrCreateUserByProfile( profile: UserProfile, ): Promise<{ user: User; created: boolean }> { const existing = await this.prisma.user.findUnique({ where: { email: profile.email }, }); if (existing) { return { user: existing, created: false }; }
const user = await this.prisma.user.create({ data: { name: profile.name, email: profile.email, password: '', }, });
return { user, created: true }; }
extractUserIdFromUser(user: User): number { return user.id; }}本指南延续之前几篇指南的账号模型:用 email 标识用户。OAuth 适配器用这个邮箱来登录已有用户,或者创建新用户。你可以为自己的应用调整这个匹配逻辑。
注册功能
更新 features.ts,把 OAuthAdapter 和驱动类传给 oauthFeature:
import { BlacklistAdapter } from './adapters/blacklist.adapter';import { CoreAdapter } from './adapters/core.adapter';import { CredentialsAdapter } from './adapters/credentials.adapter';import { OAuthAdapter } from './adapters/oauth.adapter';import { SessionAdapter } from './adapters/session.adapter';import { FeatureConfig } from './common/interfaces';import { blacklistFeature } from './features/blacklist/blacklist.feature';import { coreFeature } from './features/core/core.feature';import { credentialsFeature } from './features/credentials/credentials.feature';import { GoogleOAuthDriver } from './features/oauth/drivers/google.driver';import { GithubOAuthDriver } from './features/oauth/drivers/github.driver';import { oauthFeature } from './features/oauth/oauth.feature';import { sessionFeature } from './features/session/session.feature';
export const features: FeatureConfig[] = [ coreFeature(CoreAdapter), blacklistFeature(BlacklistAdapter), credentialsFeature(CredentialsAdapter), sessionFeature(SessionAdapter), oauthFeature(OAuthAdapter, GoogleOAuthDriver, GithubOAuthDriver),];只有传给 oauthFeature 的驱动才会被启用。你可以在项目中保留某个驱动文件而不在这里启用它。
启动应用:
pnpm start:devnpm run start:devyarn start:devoauth 功能新增一个基于提供商的端点:
| Method | Path | Description |
|---|---|---|
POST |
/auth/oauth/:provider |
通过某个 OAuth 提供商登录 |
:provider 的值必须匹配一个已启用的驱动,比如 google 或 github。
从你的前端或临时测试流程获取一个 Google idToken,然后发送给服务端:
POST /auth/oauth/googleContent-Type: application/json
{ "idToken": "<google-id-token>"}GitHub
从你的前端或临时测试流程获取一个 GitHub 授权 code,然后发送给服务端:
POST /auth/oauth/githubContent-Type: application/json
{ "code": "<github-code>"}如果邮箱已存在,请求会登录该用户;如果不存在,会先创建一个新用户。两种情况返回的令牌结果和其他登录方式一样。