跳转到内容

开始使用

构建一个具备注册、登录、登出、刷新令牌和无状态 JWT 身份验证的 NestJS 项目。

本指南会带你搭建一个使用 brkpt-auth 的最小化 NestJS 应用。

你将从一个新的 NestJS 项目开始,初始化 brkpt-auth,配置 JWT 身份验证,实现所需的适配器,并通过 credentials 功能启用账号密码登录。

完成本指南后,你的应用将支持:

  • POST /auth/sign-up
  • POST /auth/sign-in
  • POST /auth/sign-out
  • POST /auth/refresh
  • GET /auth/me
  • Node.js 20+

安装 NestJS CLI 并创建新项目:

Terminal window
pnpm add -g @nestjs/cli

创建一个新的 NestJS 项目:

Terminal window
nest new brkpt-auth-get-started --strict

然后进入项目目录:

Terminal window
cd brkpt-auth-get-started

你可以删除默认的 app.controller.ts、app.controller.spec.ts 和 app.service.ts 文件,本指南不会用到它们。

全局安装 brkpt CLI:

Terminal window
pnpm add -g @brkpt/cli

在项目根目录运行初始化命令:

Terminal window
brkpt auth init

CLI 会检测你的项目结构,安装 brkpt-auth 源文件,并列出你需要补充安装的缺失依赖。

core 功能默认会被添加,它提供基础的 JWT 流程,以及 /auth/me、/auth/refresh、/auth/sign-out 等共享端点。

core 功能需要为 access token 和 refresh token 分别配置。

安装依赖

安装本指南用到的依赖:

Terminal window
pnpm add @nestjs/config @nestjs/event-emitter cookie-parser
pnpm add -D @types/cookie-parser

创建环境变量

在项目根目录创建 .env 文件:

.env
JWT_ACCESS_SECRET="your-access-secret"
JWT_REFRESH_SECRET="your-refresh-secret"

注册 BrkptAuthModule

在 app.module.ts 中注册 ConfigModule、EventEmitterModule 和 BrkptAuthModule:

src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { BrkptAuthModule } from './brkpt-auth/brkpt-auth.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot({
global: true,
wildcard: true,
}),
BrkptAuthModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
jwt: {
access: {
secret: config.getOrThrow('JWT_ACCESS_SECRET'),
expiresIn: '5m',
},
refresh: {
secret: config.getOrThrow('JWT_REFRESH_SECRET'),
expiresIn: '1h',
transport: 'cookie',
},
},
}),
}),
],
})
export class AppModule {}

本指南使用 cookie 方式传输 refresh token。这种模式下,brkpt-auth 会把 refresh token 写入 HttpOnly cookie,客户端不需要手动处理它。

启用 cookie-parser

在 main.ts 中启用 cookie-parser:

src/main.ts
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();

brkpt-auth 不要求特定的数据库 schema、用户模型、ORM 或 repository 结构。

本指南在 UserModule 里用一个小型内存 repository,这样你无需搭建数据库就能跑通身份验证流程。

在实际应用中,你的 UserModule 通常会提供基于 ORM 的服务或 repository,比如 Prisma service、TypeORM repository,或者你自己的用户数据访问 provider。

创建以下文件:

  • 文件夹src/
    • 文件夹user/
      • 文件夹repositories/
        • memory-user.repository.ts
      • user.entity.ts
      • user.module.ts

定义用户类型

创建 user.entity.ts,定义本指南使用的用户结构:

src/user/user.entity.ts
export interface User {
id: number;
name: string;
email: string;
password: string;
}

brkpt-auth 不要求固定的用户结构。本指南的示例应用使用 id、name、email 和 password。你后面编写的适配器会决定哪些字段用于 JWT 载荷、密码校验和安全的用户响应。

创建 MemoryUserRepository

把 memory-user.repository.ts 创建为一个小型内存 repository:

src/user/repositories/memory-user.repository.ts
import { Injectable } from '@nestjs/common';
import { User } from '../user.entity';
@Injectable()
export class MemoryUserRepository {
private users: User[] = [];
private currentId = 1;
findOne(predicate: (u: User) => boolean): Promise<User | null> {
return Promise.resolve(this.users.find(predicate) || null);
}
findAll(): Promise<User[]> {
return Promise.resolve([...this.users]);
}
create(data: Omit<User, 'id'>): Promise<User> {
const newUser: User = {
id: this.currentId++,
...data,
};
this.users.push(newUser);
return Promise.resolve(newUser);
}
update(id: number, data: Partial<Omit<User, 'id'>>): Promise<User> {
const userIndex = this.users.findIndex((u) => u.id === id);
if (userIndex === -1) {
throw new Error(`User with id ${id} not found`);
}
const updated = { ...this.users[userIndex], ...data };
this.users[userIndex] = updated;
return Promise.resolve(updated);
}
delete(id: number): Promise<User> {
const userIndex = this.users.findIndex((u) => u.id === id);
if (userIndex === -1) {
throw new Error(`User with id ${id} not found`);
}
const [deleted] = this.users.splice(userIndex, 1);
return Promise.resolve(deleted);
}
}

创建 UserModule

创建 user.module.ts,导出 MemoryUserRepository 以便适配器可以注入它:

src/user/user.module.ts
import { Module } from '@nestjs/common';
import { MemoryUserRepository } from './repositories/memory-user.repository';
@Module({
providers: [MemoryUserRepository],
exports: [MemoryUserRepository],
})
export class UserModule {}

core 功能负责基础的 JWT 流程,但它并不知道你的用户模型长什么样。core 适配器告诉 brkpt-auth 如何把用户映射为 JWT 载荷、如何从 JWT 载荷查找用户,以及如何返回安全的用户数据。

创建以下文件:

  • 文件夹src/
    • 文件夹brkpt-auth/
      • 文件夹adapters/
        • core.adapter.ts
        • types.ts

定义 JWT 载荷

创建 types.ts:

src/brkpt-auth/adapters/types.ts
export type AuthJwtPayload = {
sub: number;
email: string;
};

实现 CoreAdapter

创建 core.adapter.ts:

src/brkpt-auth/adapters/core.adapter.ts
import { Injectable } from '@nestjs/common';
import { MemoryUserRepository } from '../../user/repositories/memory-user.repository';
import { User } from '../../user/user.entity';
import { CorePort } from '../features/core/core.port';
import { AuthJwtPayload } from './types';
@Injectable()
export class CoreAdapter implements CorePort<User> {
constructor(private readonly userRepo: MemoryUserRepository) {}
mapUserToJwtPayload(user: User): AuthJwtPayload {
return {
sub: user.id,
email: user.email,
};
}
shrinkJwtPayload(payload: AuthJwtPayload): Record<string, unknown> {
return {
sub: payload.sub,
};
}
findUserByJwtPayload(payload: AuthJwtPayload): Promise<User | null> {
return this.userRepo.findOne((u) => u.id === payload.sub);
}
toSafeUser(user: User): Record<string, unknown> {
const { password: _password, ...safe } = user;
return safe;
}
extractUserIdFromJwtPayload(payload: AuthJwtPayload): number {
return payload.sub;
}
}

每个方法要么是简单的映射,要么是对用户 repository 的直接调用。

注册功能

更新 features.ts,把 CoreAdapter 传给 coreFeature:

src/brkpt-auth/features.ts
import { CoreAdapter } from './adapters/core.adapter';
import { FeatureConfig } from './common/interfaces';
import { coreFeature } from './features/core/core.feature';
export const features: FeatureConfig[] = [coreFeature(CoreAdapter)];

导入 UserModule

CoreAdapter 依赖 MemoryUserRepository,而它是由 UserModule 导出的。

把 UserModule 添加到 brkpt-auth.module.ts:

src/brkpt-auth/brkpt-auth.module.ts
import { UserModule } from '../user/user.module';
@Module({
imports: [],
imports: [UserModule],
controllers: [...features.flatMap((f) => f.controllers)],
providers: [...features.flatMap((f) => f.providers)],
})
export class BrkptAuthModule {
// ...
}

到这一步,core 功能已经能从 JWT 载荷解析用户并返回安全的用户数据。

如果此时启动应用并调用 GET /auth/me,会返回 401 Unauthorized。这是预期行为,因为应用还没有注册或登录端点。

core 功能不包含账号注册或登录,这是有意为之:你可以自行选择应用需要的登录方式。

本指南使用 credentials 功能实现账号密码登录。

添加功能

运行 CLI 命令:

Terminal window
brkpt auth add credentials

CLI 会添加一个新的 features/credentials/ 文件夹。

定义 DTO

credentials 功能默认生成空的 DTO 类。brkpt-auth 不会假设你的注册和登录请求使用哪些字段。

更新 sign-up.dto.ts:

src/brkpt-auth/features/credentials/dto/sign-up.dto.ts
export class SignUpDto {
name!: string;
email!: string;
password!: string;
}

更新 sign-in.dto.ts:

src/brkpt-auth/features/credentials/dto/sign-in.dto.ts
export class SignInDto {
email!: string;
password!: string;
}

安装 bcrypt

本指南用 bcrypt 来哈希和校验密码:

Terminal window
pnpm add bcrypt
pnpm add -D @types/bcrypt

实现 CredentialsAdapter

创建 credentials.adapter.ts:

src/brkpt-auth/adapters/credentials.adapter.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { MemoryUserRepository } from '../../user/repositories/memory-user.repository';
import { User } from '../../user/user.entity';
import { CredentialsPort } from '../features/credentials/credentials.port';
import { SignInDto } from '../features/credentials/dto/sign-in.dto';
import { SignUpDto } from '../features/credentials/dto/sign-up.dto';
@Injectable()
export class CredentialsAdapter implements CredentialsPort<User> {
constructor(private readonly userRepo: MemoryUserRepository) {}
findUserByDto(dto: SignInDto | SignUpDto): Promise<User | null> {
return this.userRepo.findOne((u) => u.email === dto.email);
}
validatePassword(user: User, dto: SignInDto): Promise<boolean> {
return bcrypt.compare(dto.password, user.password);
}
async createUser(dto: SignUpDto): Promise<User> {
const password = await bcrypt.hash(dto.password, 10);
return this.userRepo.create({
name: dto.name,
email: dto.email,
password: password,
});
}
extractUserIdFromUser(user: User): number {
return user.id;
}
}

密码策略完全由适配器控制。只需修改 validatePassword 和 createUser,就能把 bcrypt 换成 argon2 或其他任意哈希库。

注册功能

更新 features.ts,把 CredentialsAdapter 传给 credentialsFeature:

src/brkpt-auth/features.ts
import { CoreAdapter } from './adapters/core.adapter';
import { CredentialsAdapter } from './adapters/credentials.adapter';
import { FeatureConfig } from './common/interfaces';
import { coreFeature } from './features/core/core.feature';
import { credentialsFeature } from './features/credentials/credentials.feature';
export const features: FeatureConfig[] = [
coreFeature(CoreAdapter),
credentialsFeature(CredentialsAdapter),
];

CredentialsAdapter 用的是同一个 MemoryUserRepository,不需要额外导入其他模块。

brkpt-auth 不强制要求使用某个校验库。本指南使用标准的 NestJS ValidationPipe 配合 class-validator。

安装校验依赖

安装依赖:

Terminal window
pnpm add class-validator class-transformer

启用 ValidationPipe

在 main.ts 中启用全局校验管道:

src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();

为 DTO 添加校验

为 sign-up.dto.ts 添加校验装饰器:

src/brkpt-auth/features/credentials/dto/sign-up.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class SignUpDto {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
@MinLength(6)
password!: string;
}

为 sign-in.dto.ts 添加校验装饰器:

src/brkpt-auth/features/credentials/dto/sign-in.dto.ts
import { IsEmail, IsString } from 'class-validator';
export class SignInDto {
@IsEmail()
email!: string;
@IsString()
password!: string;
}

启动应用:

Terminal window
pnpm start:dev

现在可以使用以下端点:

Method Path Description
POST /auth/sign-up 创建用户
POST /auth/sign-in 登录
POST /auth/sign-out 登出
POST /auth/refresh 签发新的 access token
GET /auth/me 返回当前用户

用任意 HTTP 客户端测试这些端点,比如 Postman、Bruno 或 Insomnia。

注册

发送注册请求:

POST /auth/sign-up
Content-Type: application/json
{
"name": "Kevin",
"email": "[email protected]",
"password": "password123"
}

响应中包含 accessToken,并把 refresh token 设置为 HttpOnly cookie。

登录

用同一个账号发送登录请求:

POST /auth/sign-in
Content-Type: application/json
{
"email": "[email protected]",
"password": "password123"
}

和注册一样,响应中包含 accessToken,并设置 refresh token cookie。

获取当前用户

带上 access token 调用 /auth/me:

GET /auth/me
Authorization: Bearer <access-token>

响应返回当前用户,不含密码字段。

刷新 access token

带上注册或登录时设置的 refresh token cookie,调用 /auth/refresh:

POST /auth/refresh
Cookie: refreshToken=<refresh-token>

响应中包含新的 accessToken。

登出

带上 access token 调用 /auth/sign-out:

POST /auth/sign-out
Authorization: Bearer <access-token>

在 cookie 传输模式下,登出会清除 refresh token cookie(如果存在的话)。

按顺序继续阅读后续指南,替换内存 repository、添加会话管理,并逐个功能地扩展你的身份验证流程。