nest学习笔记
controller 控制层
用于处理路由请求
路由的三种参数 Param Query Body
Param: restful API 参数
Query:url 参数
Body:post 参数
Param 就是获取 /xxx/:id
@Get('/data/:id')
getData(@Param() param): string {
console.log('🚀 ~ AppController ~ getData ~ param:', param)
return 'data' + param.id
}
@Controller('user')
export class UserController {
@Get('/:id')
// param 传递 id 使用 nest 自带的管道进行数据转换
getUser(@Param('id', ParseIntPipe) id: number) {
console.log('🚀 ~ UserController ~ id:', typeof id)
return 'get user' + id
}
}
Body
@Post('/data')
postData(@Body() body): string {
console.log('🚀 ~ AppController ~ postData ~ body:', body)
return 'post data'
}
@Post('add')
addProduct(
@Body('name') name: string, // 只取 body.name
@Body('price', ParseFloatPipe) price: number, // 再取 body.price 并转浮点
): string {
console.log(name, price); // 例:T-shirt 19.9
return `created ${name}`;
}
Query /xxx/xxx?id=xxx¶m=222
@Get('/data/:id')
getData(@Param() param, @Query() query): string {
console.log('🚀 ~ AppController ~ getData ~ param:', param)
console.log('🚀 ~ AppController ~ getData ~ query:', query)
return 'data' + param.id
}
providers 提供函数方法
就是提供方法 哪里需要使用这个方法就在哪里进行注入
其实拿 koa2 来进行举例就是
controller 是进行路由对接的 然后执行具体的函数方法 是在 service 里面执行的 数据库的修改就是进一层的里面
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@Module({
imports: [],
controllers: [AppController],
providers: [AppService], // 将service方法进行提供注入到这个module这样这个module就能全局使用
})
export class AppModule {}
@Get('/data/:id')
getData(@Param() param): string {
return this.appService.getData(param)
}
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getData(param: any) {
console.log('🚀 ~ AppService ~ getData ~ param:', param)
return 'data' + param.id
}
}
异常过滤器
// /common/exception/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
response.status(status).json({
type: 'error',
data: `[${status} - ${request.url}]`,
message: exception.message,
})
}
}
main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { HttpExceptionFilter } from './common/exception/http-exception.filter'
import { VersioningType } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('api') // 定义全局抬头
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' }) // 定义版本 /api/v1/xxx
app.useGlobalFilters(new HttpExceptionFilter()) // 设置全局错误拦截器
await app.listen(3000)
}
bootstrap().catch((err) => console.error(err))
config 配置文件
安装配置依赖
pnpm add @nestjs/config
根目录下创建配置文件 .env (该文件不进行提交) 我们创建.env.example 进行提交到代码仓库
.env
APP_NAME=nest-admin
# 数据库配置
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=
DB_SYNCHRONIZE=false
创建config模块文件
./common/config
// app.config.ts
import { registerAs } from '@nestjs/config'
export default registerAs('app', () => {
return {
name: process.env.APP_NAME,
// port: 3000,
}
})
// db.config.ts
import { registerAs } from '@nestjs/config'
export default registerAs('database', () => {
return {
type: process.env.DB_TYPE || 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 3306,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.DB_SYNCHRONIZE === 'true',
}
})
// index.ts 导出
import appConfig from './app.config'
import dbConfig from './db.config'
export default [appConfig, dbConfig]
再app.module.ts 进行导入使用
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { UserModule } from './modules/user/user.module'
import { AuthController } from './modules/auth/auth.controller'
import { AuthModule } from './modules/auth/auth.module'
import { ConfigModule } from '@nestjs/config'
import config from './common/config' // 导出的文件引入
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [...config], // 加载到全局
}),
UserModule,
AuthModule,
],
controllers: [AppController, AuthController],
providers: [AppService],
})
export class AppModule {}
再具体的模块之中进行使用
import { Controller, Get, Inject } from '@nestjs/common'
import { ConfigType } from '@nestjs/config'
import appConfig from './common/config/app.config'
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
// 使用接收全局的注入 然后使用类型工具进行包裹 这样就会有类型提示
@Inject(appConfig.KEY) private readonly app: ConfigType<typeof appConfig>,
) {}
@Get('/test')
getTest(): string {
console.log('🚀 ~ AppController ~ getTest ~ this.app.name:', this.app)
return this.app.name
}
}
集成SWC
swc 可以让我们项目的构建启动更快
pnpm i -D @swc/cli @swc/core
nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
+"builder": "swc"
}
}
新增打包命令
# 表示打包的时候 会进行类型检查
"build:type-check": "nest build --type-check",
创建配置文件 方便进行定制化
# .swcrc
{
"$schema": "https://swc.rs/schema.json",
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true,
"dynamicImport": true
},
"baseUrl": "./"
},
// 给机器最快执行时候设置为true
"minify": false
}
typeorm集成数据库
mysql数据库创建
数据库名: admin-dev
字符集: utf8
排序规则: utf8 general ci
pnpm install --save @nestjs/typeorm typeorm mysql2
- 连接数据库
// app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { UserModule } from './modules/user/user.module'
import { AuthController } from './modules/auth/auth.controller'
import { AuthModule } from './modules/auth/auth.module'
import { ConfigModule, ConfigService } from '@nestjs/config'
import config from './common/config'
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [...config],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) =>
({
type: configService.get('database.type'),
host: configService.get('database.host'),
port: configService.get('database.port'),
username: configService.get('database.username'),
password: configService.get('database.password'),
database: configService.get('database.database'),
synchronize: configService.get('database.synchronize'), // 是否自动同步 建议关闭 生产环境绝对不能使用
autoLoadEntities: true, // 自动加载实体
}) as TypeOrmModuleOptions,
}),
UserModule,
AuthModule,
],
controllers: [AppController, AuthController],
providers: [AppService],
})
export class AppModule {}
- 定义数据模型
// user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'
@Entity('admin_user')
export class User {
@PrimaryGeneratedColumn() //主键并且自增
id: number
@Column()
@Unique(['username'])
username: string
@Column()
password: string
@Column()
role: string
@Column()
nickname: string
@Column()
active: boolean
}
- 当前哪一个module需要使用就引入 实体
// user.module.ts
import { Module } from '@nestjs/common'
import { UserController } from './user.controller'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from './user.entity'
import { UserService } from './user.service'
@Module({
imports: [TypeOrmModule.forFeature([User])], // 引入需要使用的数据模型(实体)
controllers: [UserController],
providers: [UserService], // 再Service使用具体的 数据模型操作数据库数据
})
export class UserModule {}
- 再具体模块的service进行实体使用
// userService
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { User } from './user.entity'
import { Repository } from 'typeorm'
@Injectable()
export class UserService {
constructor(
// 进行注解加上实体 使用实例来进行使用
@InjectRepository(User)
private readonly userRepository: Repository<User>, // 定义类型
) {}
findOne(id: number) {
// 执行具体的sql语句查询
return this.userRepository.findOne({ where: { id } })
}
}
- service 查询然后返回给controller进行使用
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'
import { UserService } from './user.service'
@Controller('user')
export class UserController {
// 将逻辑返回给 controller 进行使用
constructor(private readonly userService: UserService) {}
@Get('/:id')
// param 传递 id 使用 nest 自带的管道进行数据转换
getUser(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id)
}
}
守卫/全局守卫
nest g gu modules/auth
// auth.module.ts
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { APP_GUARD } from '@nestjs/core'
import { AuthGuard } from './auth.guard'
@Module({
controllers: [AuthController],
providers: [
AuthService,
{
// 定义全局守卫
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AuthModule {}
如果只是定义单个的守卫 那么就不需要再 这个module里面写任何东西 只需要创建一个守卫 然后哪里需要哪里注入即可
这里我们注册的是全局路由守卫 就是每一个路由请求都会进入到这个守卫里面
- 定义一个聚合装饰器,将标记公共装饰器和守卫装饰器聚合
// auth.decorator.ts
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from './auth.guard'
export const IS_PUBLIC_KEY = 'isPublic'
export function Auth() {
// 这是聚合装饰器 表示可以聚合多个装饰器 这样外部只需要调用一个装饰器即可
// 第一个装饰器作用是设置元数据 表示这是一个公共路由
// 第二个装饰器作用是使用全局守卫 表示需要进行认证
return applyDecorators(SetMetadata(IS_PUBLIC_KEY, true), UseGuards(AuthGuard))
}
/**
这里第一个装饰器 设置了 当前路由为公共路由,
然后我们再第一个装饰器就去判断是否设置为了公共路由设置了就会放行没有就会进行第二步判断
*/
- 全局守卫(基础版本) 固定写法课参照文档全局守卫
// auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs'
import { IS_PUBLIC_KEY } from './auth.decorator'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
])
// 如果是公共路由,直接返回 true 不会被全局守卫拦截
if (isPublic) {
return true
}
return false
}
}
使用
// auth.service.ts
import { Controller, Post } from '@nestjs/common'
import { Auth } from './auth.decorator'
@Controller('auth')
export class AuthController {
@Auth() // 使用装饰器
@Post('login')
login() {
return 'login'
}
}
swagger 集成
pnpm install --save @nestjs/swagger
# 美化样式
pnpm install @scalar/nestjs-api-reference
// common/utils/setup-swagger
import { Logger } from '@nestjs/common'
import { NestExpressApplication } from '@nestjs/platform-express'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { apiReference } from '@scalar/nestjs-api-reference'
const logger = new Logger('Swagger')
export function setupSwagger(app: NestExpressApplication, port: number) {
const config = new DocumentBuilder()
.setTitle('Nest Template API')
.setDescription('这是一个基于Nest.js的模板项目API文档')
.setVersion('1.0')
.addTag('nest-template')
.build()
const documentFactory = () => SwaggerModule.createDocument(app, config)
SwaggerModule.setup('api-docs', app, documentFactory, {
jsonDocumentUrl: 'swagger/json',
})
app.use('/reference', apiReference({ url: '/swagger/json', theme: 'default', layout: 'modern' }))
logger.log(`Swagger 文档访问地址:http://localhost:${port}/reference`)
}
main.ts 中使用
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { HttpExceptionFilter } from './common/exception/http-exception.filter'
import { setupSwagger } from './common/utils/setup-swagger'
import { NestExpressApplication } from '@nestjs/platform-express'
// import { VersioningType } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule)
app.setGlobalPrefix('api')
// app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' })
app.useGlobalFilters(new HttpExceptionFilter())
// 设置api文档访问地址
const port = Number(process.env.PORT || 3000)
setupSwagger(app, port)
await app.listen(port)
}
bootstrap().catch((err) => console.error(err))
DTO 数据校验
pnpm i --save class-validator class-transformer
main.ts
// 开启全局校验管道 whitelist: true 自动过滤掉所有多余字段
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
书写DTO文件
// /auth/dto/signin-user.dto.ts
import { IsNotEmpty, IsString, Length } from 'class-validator'
export class SigninUserDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsString({ message: '用户名必须是字符串' })
@Length(4, 20, { message: '用户名长度必须在4-20之间' })
username: string
@IsNotEmpty({ message: '密码不能为空' })
@IsString({ message: '密码必须是字符串' })
@Length(6, 20, { message: '用户名长度必须在6-20之间' })
password: string
}
这里需要修改异常拦截器 对 400 错误做单独处理
// /common/exception/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, BadRequestException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
//自定义异常处理 处理 400 校验错误
if (exception instanceof BadRequestException) {
// return response.json(exception.getResponse())
const responseBody = exception.getResponse() as Record<string, any>
response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
type: 'error',
errors: responseBody.message,
message: '参数校验失败',
})
return
}
response.status(status).json({
type: 'error',
data: `[${status} - ${request.url}]`,
message: exception.message,
})
}
}
类型补充完善版
// /common/exception/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, BadRequestException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
//自定义异常处理 处理 400 校验错误
if (exception instanceof BadRequestException) {
// return response.json(exception.getResponse())
const responseBody = exception.getResponse() as Record<string, any>
const errors = responseBody.message as string[] | string
response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
type: 'error',
errors: Array.isArray(errors) ? errors : [errors],
message: '参数校验失败',
})
return
}
response.status(status).json({
type: 'error',
data: `[${status} - ${request.url}]`,
message: exception.message,
})
}
}
验证失败返回:
{
"type": "error",
"errors": [
"用户名长度必须在4-20之间",
"用户名必须是字符串",
"用户名不能为空",
"用户名长度必须在6-20之间",
"密码必须是字符串",
"密码不能为空"
],
"message": "参数校验失败"
}
JWT 鉴权
pnpm install @nestjs/jwt passport-jwt passport @nestjs/passport
pnpm install -D @types/passport-jwt
生成token信息
生成token
// auth.module.ts
import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { UserModule } from '../user/user.module'
import { JwtModule } from '@nestjs/jwt'
import { ConfigService } from '@nestjs/config'
import { JwtStrategy } from './strategies/jwt.strategy'
@Module({
imports: [
UserModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
global: true,
secret: config.get('app.token_secret'),
signOptions: { expiresIn: '1d' }, // 一天后过期
}
},
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy], // 策略文件
})
export class AuthModule {}
获取令牌
// auth.service.ts
async login(user: SigninDto) {
const isExist = await this.db.query.userSchema.findMany({
where: eq(userSchema.account, user.account),
})
if (isExist.length === 0) {
throw new BadRequestException('账号不存在')
}
const psMatch = await argon2.verify(isExist[0].password, user.password)
if (!psMatch) {
throw new BadRequestException('密码错误')
}
// 生成token进行返回
const payload = { account: user.account, sub: isExist[0].id }
const token = await this.jwtService.signAsync(payload)
return { token }
}
获取token信息进行校验
/modules/auth/strategies/jwt.strategy.ts
编写策略文件
// jwt.strategy.ts
import { ConfigService } from '@nestjs/config'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Inject, Injectable } from '@nestjs/common'
import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { userSchema } from '@/database/drizzle/schemas'
import { eq } from 'drizzle-orm'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
configService: ConfigService,
@Inject(Drizzle) private readonly db: DrizzleDB,
) {
super({
//解析用户提交的header中的Bearer Token数据
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
//加密码的 secret
secretOrKey: configService.getOrThrow('TOKEN_SECRET'),
})
}
//验证通过后获取用户资料
async validate({ sub: id }: { sub: number }) {
const [user] = await this.db.select().from(userSchema).where(eq(userSchema.id, id)).limit(1)
return user ?? null
}
}
然后再 auth.module.ts 进行注入
import { JwtStrategy } from './strategy';
...
@Module({
...
controllers: [AuthController],
//注入容器
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
简化操作
src/decorator
├── auth.decorator.ts 验证装饰器
└── user.decorator.ts 用户资料装饰器
import { applyDecorators, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
export function Auth() {
// 聚合装饰器
return applyDecorators(UseGuards(AuthGuard('jwt')))
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
然后在 user.controller.ts 等控制器中使用
import { Auth,User } from 'src/auth/decorator';
...
@Controller('user')
export class UserController {
...
@Auth()
findOne(@User() user: users) {
return user;
}
}
守卫
nest g gu modules/auth/guards/roles --flat --no-spec
这里创建一个角色守卫
拦截器-格式化信息
响应拦截器,后端返回特定的格式数据
nest g itc common/interceptors/transform --flat --no-spec
响应拦截器
// common/interceptors/transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { map, Observable } from 'rxjs'
interface IResponse<T> {
type: string
data: T
message: string
}
@Injectable()
export class TransformInterceptor<T = any> implements NestInterceptor<T, IResponse<T>> {
intercept(_: ExecutionContext, next: CallHandler<T>): Observable<IResponse<T>> {
return next.handle().pipe(
map((data) => {
return {
type: 'success',
data: data,
message: '操作成功',
}
}),
)
}
}
补充写法,支持新增、删除等操作返回 data: null
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { map, Observable } from 'rxjs'
interface IResponse<T> {
type: string
data: T | null
message: string
}
@Injectable()
export class TransformInterceptor<T = any> implements NestInterceptor<T, IResponse<T>> {
intercept(_: ExecutionContext, next: CallHandler<T>): Observable<IResponse<T>> {
return next.handle().pipe(
map((data) => {
return {
type: 'success',
data: data ?? null,
message: '操作成功',
}
}),
)
}
}
拦截器-序列化
将需要返回的数据进行定义,比如密码就不需要进行返回给前端 参考文档
过滤选项
我们使用 @SerializeOptions() 对响进行配置,然后在响应类中对属性进行细节定义。
| 选项 | 说明 | 示例 |
|---|---|---|
| strategy | exposeAll:包含所有属性 excludeAll:排除所有属性 | @SerializeOptions({ strategy: 'exposeAll' }) |
| excludePrefixes | 排除某个前缀的属性 | @SerializeOptions({ strategy: 'exposeAll',excludePrefixes: ['_'] }) |
使用官方自带的序列化方式:
main.ts 全局定义序列化
// main.ts
import { NestFactory, Reflector } from '@nestjs/core'
import { AppModule } from './app.module'
import { HttpExceptionFilter } from './common/exception/http-exception.filter'
import { TransformInterceptor } from './common/transform/transform.interceptor'
import { setupSwagger } from './common/utils/setup-swagger'
import { NestExpressApplication } from '@nestjs/platform-express'
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule)
app.useGlobalFilters(new HttpExceptionFilter()) // 设置全局错误拦截器
+app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) //全局定义序列化拦截器
app.useGlobalInterceptors(new TransformInterceptor()) // 设置全局响应拦截器
}
bootstrap().catch((err) => console.error(err))
定义模型 /auth/entitys/user.entitys.ts
import { Exclude } from 'class-transformer'
import { userSchema } from '@/database/drizzle/schemas'
export default class UserEntity {
id: number
account: string
@Exclude() // 排除 password 字段
password: string
//构造函数用于传递序列化类数据
constructor(options: Partial<typeof userSchema.$inferInsert> = {}) {
Object.assign(this, options)
}
}
实际序列化的 请求接口
import { Controller, Get, SerializeOptions } from '@nestjs/common'
import { UserService } from './user.service'
import { Auth } from '@/decorator/auth.decorator'
import { User } from '@/decorator/user.decorator'
import { userSchema } from '@/database/drizzle/schemas'
+import UserEntity from './entitys/user.entitys'
@Controller('user')
@Auth() // 表示该控制器下的所有接口都需要进行鉴权
+@SerializeOptions({ strategy: 'exposeAll' }) //定义序列化选项
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
// @Auth() // 定义该接口需要进行鉴权 并且得到的用户信息存储再 express 的 request 对象内容
async findAll(@User() user: typeof userSchema.$inferInsert) {
console.log('🚀 ~ UserController ~ findAll ~ user:', user)
const users = await this.userService.finAll()
+return users.map((user) => new UserEntity(user))
}
}
自定义序列化:
# 创建拦截器文件
nest g itc common/interceptors/serialize --flat --no-spec
main.ts 不在全局进行注册
- 创建序列化拦截器
// common/interceptors/serialize.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { plainToInstance } from 'class-transformer'
import { map, Observable } from 'rxjs'
@Injectable()
export class SerializeInterceptor implements NestInterceptor {
constructor(
private dto: any,
private flag?: boolean,
) {}
intercept(_context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return plainToInstance(this.dto, data, {
// 设置为true,只返回dto中定义的属性
// Expose -> 暴露属性
// Exclude -> 排除属性
excludeExtraneousValues: this.flag, // 设置为 true 只返回加了 @Expose() 装饰器的属性
// 开启全局转换 就是返回出去的数据 可以进行自动类型转换
enableImplicitConversion: true,
})
}),
)
}
}
- 创建自定义装饰器
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor'
import { UseInterceptors } from '@nestjs/common'
export interface ClassConstructor {
new (...args: any[]): any
}
// 作用: “手动 new 拦截器”这一步藏到装饰器里
// 等价于:@UseInterceptors(new SerializeInterceptor(UserProfileDto))
export function Serialize(dto: ClassConstructor, flag?: boolean) {
return UseInterceptors(new SerializeInterceptor(dto, flag))
}
- 创建 dto
// dto/public-user.dto.ts
import { Exclude } from 'class-transformer'
export class PublicUserDto {
id: number
account: string
@Exclude()
password: string
}
- 使用
import { Controller, Get } from '@nestjs/common'
import { UserService } from './user.service'
import { Auth } from '@/decorator/auth.decorator'
import { Serialize } from '@/decorator/serialize.decorator'
import { PublicUserDto } from './dto/public-user.dto'
@Controller('user')
@Auth() // 表示该控制器下的所有接口都需要进行鉴权
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@Serialize(PublicUserDto)
async findAll() {
return this.userService.finAll()
}
}
返回数据格式:已将 password 字段进行排除
{
"type": "success",
"data": [
{
"id": 10,
"account": "admin"
}
],
"message": "操作成功"
}
自定义装饰器区别
// 工厂函数
import { applyDecorators, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
export function Auth() {
return applyDecorators(UseGuards(AuthGuard('jwt')))
}
// 原生参数装饰器
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { Request } from 'express'
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<Request>()
return request.user
})
它们都叫“装饰器”,但一个是“工厂函数”,一个是“原生参数装饰器”,底层机制完全不同,所以写法不一样。
| 场景 | 语法 | 本质 | 例子 |
|---|---|---|---|
| 类/方法/属性 | applyDecorators(...) 返回一个函数 | 普通装饰器工厂 | @Auth() |
| 函数参数 | createParamDecorator(...) 返回一个函数 | 参数装饰器 + 运行时由框架注入 | @User() |
Auth() —— 类/方法装饰器
- 目标:把
@UseGuards(AuthGuard('jwt'))包成一个更短的@Auth() - 底层:只是一个普通函数,返回装饰器函数数组(
applyDecorators干的事)。 - 你可以在里面继续包
SetMetadata、ApiBearerAuth等,完全由 Nest 反射机制处理。
@Auth() // ← 类/方法/属性级别
@Get('profile')
profile() { ... }
User —— 参数装饰器
- 目标:把
req.user拿出来,直接注入到 handler 的参数里。 - 底层:Nest 会在运行时把
ExecutionContext传给你的工厂函数,返回值绑定到参数。 - 必须用
createParamDecorator,因为普通装饰器无法拿到参数位置。
@Get('profile')
profile(@User() user: UserPayload) { ... } // ← 参数级别
@Auth()是“语法糖”——把一堆类/方法装饰器包起来。@User()是“参数注入器”——把运行时数据塞进 handler 的参数。
登录功能实现
-
理解整个调用链路
首页我们再 auth 里面需要去处理登录因为用户名是唯一的这时候就需要去查询用户表的数据
我们需要去 user 模块查询用户信息 这时候就需要带出 userService 再 authService 进行使用
// user.service.ts import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { User } from './user.entity' import { Repository } from 'typeorm' @Injectable() export class UserService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} // 这是再 authService 需要使用的查询语句 findByUsername(username: string) { return this.userRepository.findOneBy({ username }) } }// user.module.ts import { Module } from '@nestjs/common' import { UserController } from './user.controller' import { TypeOrmModule } from '@nestjs/typeorm' import { User } from './user.entity' import { UserService } from './user.service' @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UserController], providers: [UserService], exports: [UserService], // 导出操作 }) export class UserModule {}这时候我们需要再 auth.module.ts 里面进行导入
// auth.module.ts import { Module } from '@nestjs/common' import { AuthController } from './auth.controller' import { AuthService } from './auth.service' import { APP_GUARD } from '@nestjs/core' import { AuthGuard } from './auth.guard' import { UserModule } from '../user/user.module' @Module({ imports: [UserModule], // 注意:这里导入的是整个 UserModule controllers: [AuthController], providers: [ AuthService, { // 定义全局守卫 provide: APP_GUARD, useClass: AuthGuard, }, ], }) export class AuthModule {}然后我们就可以在 auth.service.ts 里面进行使用 然后将处理完成的逻辑返回给 controller 层
import { Injectable } from '@nestjs/common' import { UserService } from '../user/user.service' @Injectable() export class AuthService { // 这时候就可以 使用 userService constructor(private readonly userService: UserService) {} async login(user: SigninDto) { const isExist = await this.db.query.userSchema.findMany({ where: eq(userSchema.account, user.account), }) if (isExist.length === 0) { throw new BadRequestException('账号不存在') } const psMatch = await argon2.verify(isExist[0].password, user.password) if (!psMatch) { throw new BadRequestException('密码错误') } // 生成token进行返回 const payload = { account: user.account, sub: isExist[0].id } const token = await this.jwtService.signAsync(payload) return { token } } }
RBAC 权限控制
- 新建数据模型 schema
| 关系 | 类型 | 理由 |
|---|---|---|
| 用户 ↔ 角色 | 多对多 | 一个用户可以有多个角色,一个角色可以分配给多个用户 → 需要中间表 user_roles |
| 权限 ↔ 角色 | 多对多 | 一条权限可以属于多个角色,一个角色可以拥有多条权限 → 需要中间表 role_permissions |
// role.schema.ts
import { pgTable, serial, text, primaryKey, integer } from 'drizzle-orm/pg-core'
import { userSchema } from './user.schema'
import { relations } from 'drizzle-orm'
import { roleToPermissionsSchema } from './permission.schema'
// 角色表
export const roleSchema = pgTable('role', {
id: serial('id').primaryKey(),
name: text('name').unique().notNull(),
description: text('description'),
})
// 角色关联用户表
export const roleSchemaRelations = relations(roleSchema, ({ many }) => ({
roleToUsers: many(userToRolesSchema),
roleToPermissions: many(roleToPermissionsSchema),
}))
// 用户角色关联表
export const userToRolesSchema = pgTable(
'user_to_roles',
{
userId: integer('user_id')
.notNull()
.references(() => userSchema.id),
roleId: integer('role_id')
.notNull()
.references(() => roleSchema.id),
},
(t) => [primaryKey({ columns: [t.userId, t.roleId] })],
)
// 用户角色关联表关联角色表
export const userToRolesSchemaRelations = relations(userToRolesSchema, ({ one }) => ({
user: one(userSchema, {
fields: [userToRolesSchema.userId],
references: [userSchema.id],
}),
role: one(roleSchema, {
fields: [userToRolesSchema.roleId],
references: [roleSchema.id],
}),
}))
用户表进行反射
// user.schema.ts
import { relations } from 'drizzle-orm'
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { userToRolesSchema } from './role.schema'
export const userSchema = pgTable('user', {
id: serial('id').primaryKey(),
account: text('account').unique().notNull(),
password: text('password').notNull(),
})
// 表示一个用户可以有多个角色
export const userSchemaRelations = relations(userSchema, ({ many }) => ({
roleToUser: many(userToRolesSchema),
}))
权限表
// permission.schema.ts
import { pgTable, serial, text, primaryKey, integer } from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'
import { roleSchema } from './role.schema'
// 权限表
export const permissionSchema = pgTable('permission', {
id: serial('id').primaryKey(),
name: text('name').unique().notNull(),
action: text('action').notNull(), // action 权限操作,如:create, read, update, delete, manage
description: text('description'),
})
export const permissionSchemaRelations = relations(permissionSchema, ({ many }) => ({
roleToPermissions: many(roleToPermissionsSchema),
}))
export const roleToPermissionsSchema = pgTable(
'role_to_permissions',
{
roleId: integer('role_id')
.notNull()
.references(() => roleSchema.id),
permissionId: integer('permission_id')
.notNull()
.references(() => permissionSchema.id),
},
(t) => [primaryKey({ columns: [t.roleId, t.permissionId] })],
)
export const roleToPermissionsSchemaRelations = relations(roleToPermissionsSchema, ({ one }) => ({
role: one(roleSchema, {
fields: [roleToPermissionsSchema.roleId],
references: [roleSchema.id],
}),
permission: one(permissionSchema, {
fields: [roleToPermissionsSchema.permissionId],
references: [permissionSchema.id],
}),
}))
最后导出 !!!
实现简单的权限控制
Role枚举来表示系统中的角色
// @/common/enum/role.enum.ts
export enum Role {
// 管理员
ADMIN = 1,
// 用户
USER = 2,
}
- 定义装饰器 此装饰器表示特定的角色才可以访问该接口内容
// @/common/decorator/auth.decorator.ts
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { Role } from '../enum/role.enum'
import { RolesGuard } from '../guards/role.guard'
import { AuthGuard } from '@nestjs/passport'
export const Role_KEY = 'roles'
export function Auth(...roles: Role[]) {
// 再请求头部定义 角色 并再jwt鉴权守卫后 去进行角色判断
return applyDecorators(SetMetadata(Role_KEY, roles), UseGuards(AuthGuard('jwt'), RolesGuard))
}
- 角色守卫
// @/common/guard/role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Role } from '../enum/role.enum'
import { Role_KEY } from '../decorator/auth.decorator'
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(Role_KEY, [context.getHandler(), context.getClass()])
// 查询是否传递的 角色信息 如果没有传递就直接放行
if (!requiredRoles || requiredRoles.length === 0) {
return true
}
// 拿到当前用户的 角色信息 如果是含有该角色就放行 否词 就拦截
const { user } = context.switchToHttp().getRequest()
return requiredRoles.some((role) => user.roleIds?.includes(role))
}
}
- 使用 装饰器进行定义
import { Controller, Get } from '@nestjs/common'
import { UserService } from './user.service'
import { Auth } from '@/common/decorator/auth.decorator'
import { Serialize } from '@/common/decorator/serialize.decorator'
import { PublicUserDto } from './dto/public-user.dto'
import { Role } from '@/common/enum/role.enum'
@Controller('user')
@Auth(Role.ADMIN) // 表示该控制器下的所有接口都需要进行鉴权
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@Serialize(PublicUserDto)
async findAll() {
return this.userService.finAll()
}
@Get('test')
test() {
return 'test'
}
}
连接redis
项目中安装 redis
# docker-compose.yaml
services:
postgres:
# 拉取最新版本的 postgres 镜像
image: postgres
# 隐射容器的 5432 端口到主机的 5432 端口
ports:
- 5432:5432
# 设置容器的环境变量
environment:
POSTGRES_PASSWORD: admin123
POSTGRES_DB: nestjs-cli
redis:
# redis 镜像
image: redis
# 隐射容器的 6379 端口到主机的 6379 端口
ports:
- '6379:6379'
# 设置 redis 密码
command: redis-server --requirepass admin123
-
- 安装依赖项
pnpm install @keyv/redis keyv @nestjs/cache-manager cache-manager cacheable
-
- 创建缓存模块
创建文件cache.module.ts:
import { Module } from '@nestjs/common'
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager'
import { createKeyv } from '@keyv/redis'
import { CacheService } from './cache.service'
@Module({
imports: [
NestCacheModule.registerAsync({
useFactory: () => ({
stores: [
createKeyv({
url: 'redis://localhost:6379',
password: 'xiao666',
}),
],
}),
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
-
- 在 AppModule 中导入缓存模块
更新app.module.ts:
import { Module } from '@nestjs/common';
import { CacheModule } from './modules/config/cache/cache.module';
@Module({
imports: [
CacheModule, // Import your custom cache module
// other modules...
],
})
export class AppModule {}
-
- 创建缓存服务
创建文件cache.service.ts:
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async get<T>(key: string): Promise<T | undefined> {
return this.cacheManager.get<T>(key);
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
await this.cacheManager.set(key, value, ttl);
}
async delete(key: string): Promise<void> {
await this.cacheManager.del(key);
}
}
- 5、在CacheModule中注册CacheService
更新cache.module.ts:
import { Module } from '@nestjs/common';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
import { createKeyv } from '@keyv/redis';
import { CacheService } from './services/cache.service';
@Module({
imports: [
NestCacheModule.registerAsync({
useFactory: () => ({
stores: [createKeyv('redis://localhost:6379')],
}),
}),
],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
-
- 在目标模块(例如 TaskModule)中导入 CacheModule。
import { Module } from '@nestjs/common';
import { TaskService } from './task.service';
import { TaskRepository } from './repositories/task.repository';
import { CacheModule } from 'src/modules/config/cache/cache.module';
@Module({
imports: [CacheModule],
providers: [TaskService, TaskRepository],
})
export class TaskModule {}
-
- 在服务中使用缓存
import { Injectable, NotFoundException } from '@nestjs/common';
import { TaskRepository } from '../repositories/task.repository';
import { TaskDto } from '../dto/task.dto';
import { CacheService } from 'src/modules/config/cache/services/cache.service';
@Injectable()
export class TaskService {
constructor(
private readonly taskRepository: TaskRepository,
private readonly cache: CacheService, // Inject the CacheService
) {}
async findById(id: number): Promise<TaskDto> {
const cacheKey = `task:${id}`;
// 1. Try to get from cache
const cached = await this.cache.get<TaskDto>(cacheKey);
if (cached) {
return cached;
}
// 2. If not found in cache, fetch from database
const task = await this.taskRepository.findById(id);
if (!task) {
throw new NotFoundException('Task not found');
}
// 3. Set in cache for future requests
await this.cache.set(cacheKey, task, 300 * 1000); // 5 minutes TTL
return task;
}
}
集成邮件发送功能
-
首先,要开启 smtp、imap 等服务,这里以 qq 邮箱举例(其他邮箱也类似):在邮箱帮助中心 service.mail.qq.com/ 可以搜到如何开启 smtp、imap 等服务:
-
安装第三方依赖包
pnpm install -S nodemailer
pnpm install -D @types/nodemailer
# 创建 模块
nest g resource modules/email
- 逻辑实现 service 层
import appConfig from '@/common/config/app.config'
import { Inject, Injectable } from '@nestjs/common'
import { type ConfigType } from '@nestjs/config'
import { createTransport, Transporter } from 'nodemailer'
@Injectable()
export class EmailService {
transporter: Transporter
constructor(@Inject(appConfig.KEY) private readonly app: ConfigType<typeof appConfig>) {
this.transporter = createTransport({
host: this.app.email.host,
port: Number(this.app.email.port),
secure: false,
auth: {
user: this.app.email.user,
pass: this.app.email.password,
},
})
}
async sendEmail({ to, subject, html }) {
await this.transporter.sendMail({
from: {
name: this.app.email.from_name || '系统邮件',
address: this.app.email.user || '',
},
to,
subject,
html,
})
}
}
controller 层 定义成全局模块 @Global()
- 再注册接口进行使用
async register(registerUserDto: RegisterUserDto) {
const captcha = await this.cacheService.get(`user:${registerUserDto.email}`)
if (!captcha) {
throw new BadRequestException('验证码不存在')
}
if (captcha !== registerUserDto.captcha) {
throw new BadRequestException('验证码错误')
}
const [user] = await this.db.select().from(userSchema).where(eq(userSchema.email, registerUserDto.email))
if (user) {
throw new BadRequestException('该邮箱已被注册')
}
try {
const password = await argon2.hash(registerUserDto.password)
await this.db
.insert(userSchema)
.values({
account: registerUserDto.account,
password,
email: registerUserDto.email,
})
.returning()
return '注册成功'
} catch (error) {
this.logger.error('注册用户失败', error)
throw new HttpException('注册用户失败', HttpStatus.INTERNAL_SERVER_ERROR)
}
}
async getRegisterCaptcha(address: string) {
try {
const code = Math.random().toString().slice(2, 8)
await this.cacheService.set(`user:${address}`, code, 5 * 60 * 1000)
await this.emailService.sendEmail({
to: address,
subject: '注册验证码',
html: `<p>【凌云少年志】您的注册验证码为:${code},有效期为5分钟。</p>`,
})
return '验证码发送成功'
} catch (error) {
this.logger.error('获取验证码失败', error)
throw new HttpException('获取验证码失败', HttpStatus.INTERNAL_SERVER_ERROR)
}
}
IP以及登录信息获取
pnpm install ip2region useragent
pnpm install -D @types/useragent
封装公共方法
// common/utils/utils.ts
import IP2Region from 'ip2region'
import type { Request } from 'express'
import * as useragent from 'useragent'
/**
* 获取请求IP地址
* @param req 请求对象
* @returns IP地址
*/
export const getIp = (req: Request) => {
const ips =
(req.headers['x-forwarded-for'] as string) ||
(req.headers['X-Real-IP'] as string) ||
(req.socket.remoteAddress?.replace('::ffff:', '') as string)
if (ips === '::1') return '127.0.0.1'
return ips.split(',')?.[0] || ''
}
/**
* 获取IP地址对应的地址信息
* @param ip IP地址
* @returns 地址信息
*/
export const getAddressByIp = (ip: string): string => {
if (!ip) return ''
const query = new IP2Region()
const res = query.search(ip)
return [res?.province || '', res?.city || ''].join(' ')
}
/**
* 获取请求头中的User-Agent信息
* @param userAgent User-Agent字符串
* @returns User-Agent信息
*/
export const getUserAgent = (req: Request): { browser: string; os: string } => {
try {
const agent = useragent.parse(req.headers['user-agent'] as string)
return {
browser: `${agent.family} ${agent.major}.${agent.minor}`,
os: agent.os.family,
}
} catch {
return { browser: '', os: '' }
}
}
创建 login-log 模块
nest g resource modules/login-log
- service 层书写逻辑
import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { LoginLogCreateDto } from './dto/create.dto'
import { loginLogSchema } from '@/database/drizzle/schemas'
@Injectable()
export class LoginLogService {
private logger = new Logger()
constructor(@Inject(Drizzle) private readonly db: DrizzleDB) {}
async createLoginLog(createDto: LoginLogCreateDto) {
await this.db.insert(loginLogSchema).values(createDto)
}
}
导出供 auth 模块进行使用 !!!
创建数据模型 存储数据
// common/db/login-log.schema.ts
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core'
import { baseSchema } from './global.schema'
export const loginLogSchema = pgTable('sys_login_log', {
id: serial('id').primaryKey(),
username: text('username'),
ip: text('ip'),
address: text('address'),
browser: text('browser'),
os: text('os'),
status: integer('status').notNull().default(0),
message: text('message'),
...baseSchema,
})
auth 登录模块进行使用
import { Drizzle, type DrizzleDB } from '@/database/drizzle/database.module'
import { userSchema } from '@/database/drizzle/schemas'
import { BadRequestException, Inject, Injectable } from '@nestjs/common'
import type { Request } from 'express'
import { JwtService } from '@nestjs/jwt'
import * as argon2 from 'argon2'
import { and, eq } from 'drizzle-orm'
import { SigninDto } from './dto/signin.dto'
import { LoginLogService } from '../login-log/login-log.service'
import { LoginLogCreateDto } from '../login-log/dto/create.dto'
import { getAddressByIp, getIp, getUserAgent } from '@/common/utils/utils'
@Injectable()
export class AuthService {
constructor(
@Inject(Drizzle) private readonly db: DrizzleDB,
private readonly jwtService: JwtService,
private readonly loginLogService: LoginLogService,
) {}
/**
* 登录
* @param user 登录信息
* @param req 请求对象
* @param isAdmin 是否为管理员
* @returns 登录成功后的token
*/
async signin(user: SigninDto, req: Request, isAdmin?: number) {
const loginLog = {} as LoginLogCreateDto
loginLog.username = user.account
loginLog.ip = getIp(req)
loginLog.address = getAddressByIp(loginLog.ip)
const userAgentInfo = getUserAgent(req)
loginLog.browser = userAgentInfo.browser
loginLog.os = userAgentInfo.os
try {
const isExist = await this.db.query.userSchema.findMany({
where: and(eq(userSchema.account, user.account), eq(userSchema.is_admin, isAdmin ?? 0)),
})
if (isExist.length === 0) {
throw new BadRequestException('账号不存在')
}
const psMatch = await argon2.verify(isExist[0].password, user.password)
if (!psMatch) {
throw new BadRequestException('密码错误')
}
// 生成token进行返回
const payload = { account: user.account, sub: isExist[0].id }
const token = await this.jwtService.signAsync(payload)
// 存储登录日志
loginLog.status = 1
loginLog.message = '登录成功'
return { token }
} catch (error) {
loginLog.status = 0
loginLog.message = '登录失败'
throw new BadRequestException(error.message)
} finally {
await this.loginLogService.createLoginLog(loginLog)
}
}
}
controller层
import type { Request } from 'express'
@Post('user/login')
login(@Body() user: SigninDto, @Req() req: Request) {
const isAdmin = 0
return this.authService.signin(user, req, isAdmin)
}