创建项目
nest new project-name
安装包
pnpm i @nestjs/jwt @nestjs/typeorm typeorm mysql2 redis class-transformer class-validator @nestjs/config
src\.env
#redis相关配置
redis_server_host=localhost
redis_server_port=6379
redis_server_db=1
#短信相关配置
sign_name=填写自己的
template_code=填写自己的
access_key=填写自己的
access_key_secret=填写自己的
#mysql相关配置
mysql_server_host=localhost
mysql_server_port=3306
mysql_server_username=root
mysql_server_password=填写自己的
mysql_server_database=填写自己的
#nest服务配置
nest_server_port=3000
#jwt配置
jwt_secret=填写自己的
jwt_access_token_expires_time=30m
jwt_refresh_token_expres_time=7d
nest-cli.json
asssets是指定build时复制的文件,watchAssets是在assets变动之后自动重新复制
{
"compilerOptions": {
"deleteOutDir": true,
+ "watchAssets": true,
+ "assets": ["**/*.env"]
},
}
src\modules\redis\redis.module.ts
使用configService读取变量
import { Global, Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createClient } from 'redis';
import { RedisService } from './redis.service';
@Global()
@Module({
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
await client.connect();
return client;
},
inject: [ConfigService],
},
],
exports: [RedisService],
})
export class RedisModule {}
src\app.module.ts
挂载相关模块
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RedisModule } from './modules/redis/redis.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
ConfigModule.forRoot({
// 是否全局配置
isGlobal: true,
// 环境变量文件路径
envFilePath: 'src/.env',
}),
JwtModule.registerAsync({
// 设置全局的jwt加密密钥
global: true,
useFactory(configService: ConfigService) {
return {
secret: configService.get('jwt_secret'),
signOptions: {
expiresIn: configService.get('jwt_access_token_expires_time'), // 默认 30 分钟
},
};
},
inject: [ConfigService],
}),
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
return {
// 数据库类型
type: 'mysql',
// 数据库主机地址
host: configService.get('mysql_server_host'),
// 数据库端口
port: configService.get('mysql_server_post'),
// 数据库用户名
username: configService.get('mysql_server_username'),
// 数据库密码
password: configService.get('mysql_server_password'),
// 数据库名
database: configService.get('mysql_server_database'),
// 实体类
entities: [`${__dirname}/../modules/**/*.entity{.ts,.js}` + ''],
// 是否自动同步数据库
synchronize: true,
// 是否开启日志
logging: true,
// 连接包
connectorPackage: 'mysql2',
// 是否自动加载实体
autoLoadEntities: true,
};
},
inject: [ConfigService],
}),
RedisModule,
],
controllers: [AppController],
providers: [
AppService,
],
})
export class AppModule {}
src\main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './custom-exception.filter';
import { FormatResponseInterceptor } from './format-response.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
const configService = app.get(ConfigService);
// 全局使用管道,使用ValidationPipe
app.useGlobalPipes(new ValidationPipe());
await app.listen(configService.get('nest_server_port'));
}
bootstrap();
格式化
统一格式化响应成功、失败数据格式
格式化响应失败数据格式
生成filter文件
nest g filter custom-exception
src\custom-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
// 捕获HttpException异常
@Catch(HttpException)
export class CustomExceptionFilter implements ExceptionFilter {
// 捕获异常
catch(exception: HttpException, host: ArgumentsHost) {
// 获取响应对象
const response = host.switchToHttp().getResponse<Response>();
// 设置响应状态码
response.statusCode = exception.getStatus();
// 获取响应信息
const res = exception.getResponse() as { message: string[] };
// 返回响应
response
.json({
code: exception.getStatus(),
message: 'fail',
// 兼容ValidationPipe报错
data: res?.message?.join ? res?.message?.join(',') : exception.message,
})
.end();
}
}
格式化响应成功数据格式
生成interceptor文件
nest g interceptor format-response.interceptor
src\format-response.interceptor.ts
import {
CallHandler,
ExecutionContext,
HttpStatus,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Response } from 'express';
import { map, Observable } from 'rxjs';
@Injectable()
export class FormatResponseInterceptor implements NestInterceptor {
// 拦截上下文和下一个处理程序,返回一个Observable
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 获取响应
const response = context.switchToHttp().getResponse<Response>();
// 返回下一个处理程序处理后的数据,并处理响应状态码
return next.handle().pipe(
// 映射data,返回一个对象
map((data) => {
// 返回状态码200
response.status(200);
return {
code: HttpStatus.OK,
message: 'success',
data,
};
}),
);
}
}
挂载
src\main.ts
注册自定义异常过滤器、全局拦截器
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './custom-exception.filter';
import { FormatResponseInterceptor } from './format-response.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
const configService = app.get(ConfigService);
// 添加自定义异常过滤器
app.useGlobalFilters(new CustomExceptionFilter());
// 使用全局拦截器,添加FormatResponseInterceptor拦截器
app.useGlobalInterceptors(new FormatResponseInterceptor());
// 全局使用管道,使用ValidationPipe
app.useGlobalPipes(new ValidationPipe());
await app.listen(configService.get('nest_server_port'));
}
bootstrap();
用户模块
生成用户模块
nest g user modules/user
controller
src\modules\user\user.controller.ts
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { UserInput } from './dto/user-input.type';
import { UserType } from './dto/user.type';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async createUser(@Body() params: UserInput): Promise<boolean> {
return await this.userService.create(params);
}
@Get('/:id')
async find(@Param('id') id: string): Promise<UserType> {
return await this.userService.find(id);
}
@Post('/:id')
async update(
@Param('id') id: string,
@Body() params: UserInput,
): Promise<boolean> {
return await this.userService.update(id, params);
}
@Get('/delete')
async delete(@Param('id') id: string): Promise<boolean> {
return await this.userService.del(id);
}
}
service
src\modules\user\user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DeepPartial } from 'typeorm';
import { User } from './models/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>,
) {}
async create(entity: DeepPartial<User>): Promise<boolean> {
await this.userRepository.insert(entity);
return true;
}
async find(tel: string): Promise<User> {
const res = await this.userRepository.findOne({
where: {
tel,
},
});
return res;
}
async update(id: string, params: any) {
await this.userRepository.update(id, params);
return true;
}
async del(id: string) {
await this.userRepository.delete(id);
return true;
}
}
module
src\modules\user\user.module.ts
注入Repository
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
exports: [UserService],
controllers: [UserController],
})
export class UserModule {}
用户表字段
import { IsNotEmpty } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
comment: '昵称',
default: '',
})
@IsNotEmpty()
name: string;
@Column({
comment: '描述',
default: '',
})
desc: string;
@Column({
comment: '手机号',
nullable: true,
})
tel: string;
@Column({
comment: '头像',
nullable: true,
})
avatar: string;
}
短信服务
使用阿里云的短信服务
安装对应的包
pnpm i @alicloud/dysmsapi20170525 @alicloud/openapi-client @alicloud/tea-util
controller
生成短信模块
nest g resource modules/auth
src\modules\auth\auth.controller.ts
import {
Body,
Post,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/auth.dto';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
) {}
@Post('sendCodeMsg')
async sendCodeMsg(@Body('tel') tel: string) {
return await this.authService.sendCodeMsg(tel);
}
service
src\modules\auth\auth.service.ts
- 拿到手机号先查询数据库是否存在,不存在就创建
- 查询redis中的验证码是否过期,如果没有过期就报错
- 拿到返回的验证码保存到redis,设置300秒过期,可以减少读取数据库的频率
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import Util, * as $Util from '@alicloud/tea-util';
import { getRandomCode } from '@/utils';
import { RedisService } from '../redis/redis.service';
import { LoginDto } from './dto/auth.dto';
import { UserService } from '../user/user.service';
import { LoginUserVo } from './vo/login-user.vo';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private readonly redisService: RedisService,
private readonly userService: UserService,
) {}
// 发送短信验证码
async sendCodeMsg(phone: string): Promise<string> {
const user = await this.userService.find(phone);
if (!user) {
await this.userService.create({ tel: phone });
}
const loginMsg = await this.redisService.get(`loginMsg${phone}`);
if (loginMsg) {
throw new HttpException('请勿重复发送验证码', HttpStatus.BAD_REQUEST);
}
const code = getRandomCode();
const config = new $OpenApi.Config({
// 必填,您的 AccessKey ID
accessKeyId: this.configService.get('access_key'),
// 必填,您的 AccessKey Secret
accessKeySecret: this.configService.get('access_key_secret'),
});
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = `dysmsapi.aliyuncs.com`;
const client = new Dysmsapi20170525(config);
// 请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
signName: this.configService.get('sign_name'),
templateCode: this.configService.get('template_code'),
phoneNumbers: phone,
templateParam: `{\"code\":\"${code}\"}`,
});
const runtime = new $Util.RuntimeOptions({});
try {
// 复制代码运行请自行打印 API 的返回值
await client.sendSmsWithOptions(sendSmsRequest, runtime);
this.redisService.set(`loginMsg${phone}`, code, 300);
} catch (error) {
// 如有需要,请打印 error
Util.assertAsString(error.message);
}
return code;
}
src\utils\index.ts
生成四位随机数函数
export const getRandomCode = () => {
// 返回一个介于1000-9000之间的随机数
return (Math.floor(Math.random() * 9000) + 1000).toString();
};
module
引入user模块
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
@Module({
imports: [UserModule],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
登录接口
service
src\modules\auth\auth.service.ts
- 先判断手机号是否被注册,再判断验证码是否过期、是否与redis中的验证码相同
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import Util, * as $Util from '@alicloud/tea-util';
import { getRandomCode } from '@/utils';
import { RedisService } from '../redis/redis.service';
import { LoginDto } from './dto/auth.dto';
import { UserService } from '../user/user.service';
import { LoginUserVo } from './vo/login-user.vo';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private readonly redisService: RedisService,
private readonly userService: UserService,
private readonly configService: ConfigService,
) {}
// 登录
async login(parmas: LoginDto) {
const { tel, code } = parmas;
// 调用userService的find方法,根据tel查找用户
const user = await this.userService.find(tel);
// 如果没有找到用户,抛出异常
if (!user) {
throw new HttpException('手机号未被注册', 400);
}
// 调用redisService的get方法,根据tel获取验证码
const codeMsg = await this.redisService.get(`loginMsg${tel}`);
// 如果没有找到验证码,抛出异常
if (!codeMsg) {
throw new HttpException('验证码已过期', HttpStatus.BAD_REQUEST);
}
// 如果验证码不正确,抛出异常
if (codeMsg !== code) {
throw new HttpException('验证码不正确', HttpStatus.BAD_REQUEST);
}
// 创建LoginUserVo对象
const vo = new LoginUserVo();
// 设置vo的userInfo属性
vo.userInfo = {
id: user.id,
desc: user.desc,
name: user.name,
tel: user.tel,
avatar: user.avatar,
};
// 返回vo
return vo;
}
}
controller
src\modules\auth\auth.controller.ts
login
用户生成accessToken、refreshToken
- 在返回结果添加
accessToken、refreshToken accessToken中保存了userId、name,过期时间为30分钟refreshToken中保存了userId,过期时间为7天
import {
Body,
Controller,
Get,
Post,
Query,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../user/user.service';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/auth.dto';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
@Post('login')
async login(@Body() params: LoginDto) {
const vo = await this.authService.login(params);
vo.accessToken = this.jwtService.sign(
{
// 用户id
userId: vo.userInfo.id,
// 用户名
name: vo.userInfo.name,
},
{
// 过期时间设置为30分钟
expiresIn: '30m',
},
);
vo.refreshToken = this.jwtService.sign(
{
// 用户id
userId: vo.userInfo.id,
},
{
// 过期时间设置为7天
expiresIn: '7d',
},
);
return vo;
}
}
src\modules\auth\vo\login-user.vo.ts
返回数据类型
interface UserInfo {
id: string;
name: string;
desc: string;
tel: string;
avatar: string;
}
export class LoginUserVo {
userInfo: UserInfo;
accessToken: string;
refreshToken: string;
}
src\modules\auth\dto\auth.dto.ts
校验login接口参数
import { IsNotEmpty, IsString } from 'class-validator';
export class LoginDto {
@IsNotEmpty({
message: '手机号不能为空',
})
@IsString()
tel: string;
@IsNotEmpty({
message: '密码不能为空',
})
@IsString()
code: string;
}
refresh
用于根据refresh_token重新生成accessToken、refreshToken
- 先解析
refresh_token拿到用户的id,如果解析失败就返回token失效 - 如果没有过期,就再生成
accessToken、refreshToken
@Get('refresh')
// async 声明函数为异步函数
async refresh(@Query('refresh_token') refresh_token: string) {
try {
// 使用 jwtService 验证 refreshToken
const data = this.jwtService.verify(refresh_token);
// 使用 userService 查询用户
const user = await this.userService.find(data.id);
// 使用 jwtService 生成 access_token
const accessToken = this.jwtService.sign(
{
userId: user.id,
name: user.name,
},
{
expiresIn: '30m',
},
);
// 使用 jwtService 生成 refresh_token
const refreshToken = this.jwtService.sign(
{
userId: user.id,
},
{
expiresIn: '7d',
},
);
// 返回 access_token 和 refresh_token
return {
accessToken,
refreshToken,
};
} catch (error) {
// 如果出现错误,抛出 UnauthorizedException
throw new UnauthorizedException('token 已失效,请重新登录');
}
}