Nest 登录注册模块
前言
在学完 数据库,jwt 之后,我们可以试着用 Nest 来实现登录注册,用到的依赖有如下:
"@nestjs/jwt": "^10.1.0", // jwt
"@nestjs/typeorm": "^10.0.0", // nest 操作 typeorm
"class-transformer": "^0.5.1", // 管道校验参数
"class-validator": "^0.14.0", // 管道校验参数
"mysql2": "^3.6.0", // 数据库
"typeorm": "^0.3.17" // ORM框架操作数据库
步骤:
-
在 App 模块中,注入
typeorm
、jwt
模块 -
创建 user 模块,并完善 用户表(user) 的 实体(entity) 以及需要使用到的
DTO
类型 -
实现 user 模块的
/register
和/login
接口,并实现 service -
创建**管道(Pipe)校验参数,创建路由守卫(Guard)**用于鉴权
-
处理成功的响应(Interceptor 拦截器) 和 失败的响应(ExceptionFilter 异常处理)
1. 注入模块
// app.module.ts 文件
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'ip地址',
port: 3306,
username: '数据库账户',
password: '数据库密码',
database: '库名',
synchronize: true,
logging: true,
entities: [User], // User模块的实体
connectorPackage: 'mysql2', // 声明我们需要连接的数据库的包名,需要安装这个
extra: {
authPlugin: 'sha256_password',
},
}),
JwtModule.register({
secret: 'pengnima', // 盐
global: true, // 声明为全局模块,之后就可以在所有模块中直接注入 jwtService
signOptions: {
expiresIn: '1d',
},
}),
UserModule,
],
})
export class AppModule {}
2. 编写实体,DTO
// user.entity.ts 文件
@Entity()
export class User {
@PrimaryGeneratedColumn({
type: 'bigint',
comment: '用户ID,自增',
})
id: number
@Column({
length: 50,
comment: '用户昵称',
})
nick_name: string
@Column({
length: 20,
comment: '用户名',
})
login_name: string
@Column({
length: 50,
comment: '密码',
})
password: string
@Column({
comment: '创建时间',
type: 'bigint',
nullable: true,
})
create_time: number
@Column({
comment: '更新时间',
type: 'bigint',
nullable: true,
})
update_time: number
// 要触发 BeforeInsert 装饰器,那么需要在 service 中通过 create 或者 new User 创建一个实体实例
@BeforeInsert()
setCreateTime() {
const now = Date.now()
this.create_time = this.create_time || now
this.update_time = this.update_time || now
}
@BeforeUpdate()
setUpdateTime() {
this.update_time = Date.now()
}
}
这里要注意的是,我使用 number 类型的时间戳 来存储时间的,且这 2 个时间应该是自动设置的,所以用了 @BeforeInsert
和 @BeforeUpdate
这 2 个注解。
// create-user.dto.ts
export class CreateUserDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsString({ message: '用户名必须为字符串' })
@Length(3, 50, { message: '用户名长度必须在3到50个字符之间' })
readonly login_name: string
@IsOptional()
@IsString({ message: '昵称必须为字符串' })
@Length(3, 50, { message: '昵称长度必须在3到50个字符之间' })
readonly nick_name?: string
@IsNotEmpty({ message: '密码不能为空' })
@IsString({ message: '密码必须为字符串' })
@Matches(/^\w{6,9}$/, {
message: '密码为字母,数字,长度在6 - 9位之间',
})
readonly password: string
}
这里是 注册用户的 DTO 部分,用户只需要传递 login_name
和 password
即可。这里的 nick_name
因为有 @IsOptional
装饰器,所以可以不传入,但如果传入,则需要满足 @IsString
和 @Length
的条件。
3. 实现接口,service
// user.controller.ts
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// 注册接口,管道校验 DTO 参数
@Post('/register')
async create(@Body(ValidatePipe) createUserDto: CreateUserDto) {
return await this.userService.create(createUserDto)
}
@Post('/login')
login(@Body(ValidatePipe) data: LoginDot) {
return this.userService.login(data)
}
@Get('/list')
@UseGuards(LoginGuard) // 路由守卫,鉴权
findAll() {
return this.userService.findAll()
}
}
路由守卫 和 管道校验 就是在接口的 Controller
层中使用,接下来就看看如何实现。
4. 创建管道,路由守卫
// validate.pipe.ts
@Injectable()
export class ValidatePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
const object = plainToInstance(metadata.metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
throw new BadRequestException({
errors: (errors ?? [])?.map((v) => ({
field: v.property,
message: Object.values(v.constraints).join(','),
})),
message: '参数校验失败',
})
}
return value
}
}
// login.guard.ts
// 鉴权用
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const req = context.switchToHttp().getRequest()
const token = req.header('token')
if (!token) {
throw new UnauthorizedException('token 不存在')
}
try {
const info = this.jwtService.verify(token)
return true
} catch (error) {
throw new UnauthorizedException('token 已失效')
}
}
}
5. 响应处理,错误处理
// transform.interceptor.ts
// 成功响应的处理
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp()
const res = ctx.getResponse()
res.status(HttpStatus.OK)
return next.handle().pipe(
map((data) => {
if (isString(data) || Array.isArray(data)) {
return {
data,
code: 200,
message: 'Success',
}
}
const message = data?.message ? data?.message : 'Success'
let newData = {
...data,
message: undefined,
}
const values = Object.values(newData).filter(Boolean)
if (values.length == 1) {
newData = values?.[0]
}
return {
data: newData,
code: 200,
message,
}
})
)
}
}
// exception.filter.ts
// 异常处理
@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
console.log({ exception })
const isBadRequest = exception instanceof HttpException
const code = isBadRequest ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
const message = isBadRequest ? exception.message : '网络异常,请稍后重试'
const error = {
code,
message,
errors: isBadRequest ? (exception.getResponse() as any)?.errors : (exception as any).driverError,
}
response.status(isBadRequest ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR)
response.send(error)
}
}
由于这 2 个是通用、统一的。所以可以直接在 main.ts
中注入成全局的
app.useGlobalInterceptors(new TransformInterceptor())
app.useGlobalFilters(new HttpExceptionFilter())
总结
从使用角度总结下流程就是:
调用接口时,需要用管道(Pipe)校验下参数,校验的规则写在参数的类型(DTO)中,然后有些接口还可以有守卫,有守卫的接口先执行守卫鉴权,再执行管道校验
校验不通过 或者有代码逻辑错误,则触发 异常处理(HttpException)。
校验通过,则继续下去,走到 service
的代码,通过 userService
去操作数据库,然后通过拦截器(Interceptor) 返回统一格式的数据。