我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
TypeORM
TypeORM 是一个ORM框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用。 它的目标是始终支持最新的 JavaScript 特性并提供额外的特性以帮助你开发任何使用数据库的(不管是只有几张表的小型应用还是拥有多数据库的大型企业应用)应用程序。
TypeORM 参考了很多其他优秀 ORM 的实现, 比如 Hibernate, Doctrine 和 Entity Framework,其部分Api功能如下
详细请看官网地址: typeorm.bootcss.com/
安装与连接
npm i typeorm
注意如果连接不同的数据库也需要安装对应的数据库连接包,如本文用到的postgres,则需要
npm i pg -S
连接数据库
方式一: TypeOrmModule.forRoot
下面以连接postgresql为例
TypeOrm也支持Mongodb数据库,详细Api请看: typeorm.bootcss.com/mongodb
app.module
@Module({
imports: [CoffeesModule, TypeOrmModule.forRoot(
{
type: 'postgres',
host: '192.168.***.***',
port: 5432,
username: 'postgres',
password: '******',
database: '******',
autoLoadEntities: true,
synchronize: true
}
)],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
synchronize可以让数据库根据实体类自动生成表及元数据(严禁在生产环境中使用,会破坏表结构)
后面章节我们会通过读取配置的方式实现数据库配置
方式二: 也可以通过ormconfig.json 独立配置(不推荐)
在根目录下创建一个ormconfig.json文件(与src同级), 而不是将配置对象传递给forRoot()的方式。
然后在app.module.ts中不带任何选项的调用forRoot()
设置实例Entity
TypeOrm 可以使用两种模式, Active Record 和 Data Mapper 模式创建实例,并执行增删改查的操作。
创建实体类案例(Data Mapper 方式)
默认表名为class名称的小写,也可以通过设置@Entity(‘coffee’)来定义表名
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@Column()
recommendations: number;
@Column('json', { nullable: true })
flavors: string[];
}
(3)module注册
coffee.module
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CoffeesController } from './coffees.controller';
import { CoffeesService } from './coffees.service';
import { Coffee } from './entities/coffee.entity';
@Module({
imports: [TypeOrmModule.forFeature([Coffee])],
controllers: [CoffeesController],
providers: [CoffeesService]
})
export class CoffeesModule { }
(4)Repository操作数据库
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>
){}
(5) 创建@OneToOne 与 @OneToMany方法
下面代码中userEntity与WorkItemEntity相互之间约定了@OneToMany与@ManyToOne实现相互关联,如果只是单向关联则可以不写其中一半(如只写了user - @OneToMany - workItem, 则只能通过UserEntity查询到userWorkItemInfo,从workItemEntity则无法查询到user信息了)。
注意: cascade 需要写在ManyToOne这一侧
// user.entity.ts
@OneToMany(() => WorkItemEntity, (m) => m.userInfo)
userWorkItemInfo: WorkItemEntity[];
// workItem.entity.ts
@ManyToOne(() => UserEntity, (m) => m.userWorkItemInfo, {
cascade: true, // 注意cascade 需要写在ManyToOne这一侧
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
userInfo: UserEntity;
Active Record 在entity内定义静态方法
使用 Active Record 方法,可以在模型本身内定义所有查询方法,并使用模型方法保存、删除和加载对象。
下面是使用 Active Record 模式的样子:
- 所有 active-record 实体都必须extends BaseEntity类,它提供了与实体一起使用的方法。
- 可以在User类中创建静态方法等函数
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User extends BaseEntity { // extends BaseEntity
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
// 创建静态方法函数
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
// 示例如何保存AR实体
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
// 直接使用UserEntity 的实例 user来save
await user.save();
// 删除
await user.remove();
// 查询实体
const users = await User.find({ skip: 2, take: 5 });
const newUsers = await User.find({ isActive: true });
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });
// 使用实体类中的static 静态方法
const timber = await User.findByName("Timber", "Saw");
Data Mapper (推荐)
使用 Data Mapper 方法,你可以在称为“存储库”(Repository)的单独类中定义所有查询方法,并使用存储库保存、删除和加载对象:
const userRepository = connection.getRepository(User);
// 示例如何保存DM实体
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
// 存储库Repository实现数据操作
await userRepository.save(user);
// 删除
await userRepository.remove(user);
// 查询实体
const users = await userRepository.find({ skip: 2, take: 5 });
const newUsers = await userRepository.find({ isActive: true });
const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" });
静态方法添加: 假设我们要创建一个按 first name 和 last name 返回用户的函数(类似于上面Active Record的static静态方法),可以在"custom repository"中创建这样的功能。
import { EntityRepository, Repository } from "typeorm";
import { User } from "../entity/User";
@EntityRepository()
export class UserRepository extends Repository<User> {
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
使用哪种方式?
在软件开发中我们应该始终牢记的一件事是我们如何维护它。
- Data Mapper方法可以帮助你保持软件的可维护性,这在更大的应用程序中更有效。
- Active record方法可以帮助你保持简单,这在小型应用程序中运行更好。 简单性始终是提高可维护性的关键。
使用 cascades 自动建立表关联关系
如上面步骤缩写
下面代码中userEntity与WorkItemEntity相互之间约定了@OneToMany与@ManyToOne实现相互关联,如果只是单向关联则可以不写其中一半(如只写了user - @OneToMany - workItem, 则只能通过UserEntity查询到userWorkItemInfo,从workItemEntity则无法查询到user信息了)。
注意: cascade 需要写在ManyToOne这一侧
// user.entity.ts
@OneToMany(() => WorkItemEntity, (m) => m.userInfo)
userWorkItemInfo: WorkItemEntity[];
// workItem.entity.ts
@ManyToOne(() => UserEntity, (m) => m.userWorkItemInfo, {
cascade: true, // 注意cascade 需要写在ManyToOne这一侧
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
userInfo: UserEntity;
再举个栗子
photo 的@OneToOne装饰器:
export class Photo {
/// ... other columns
@OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
cascade: true
})
metadata: PhotoMetadata;
}
使用cascade允许就不需要手动存储 photo 和metdata两个实体。我们可以直接保存一个 photo 对象,由于使用了 cascade,关联的metadata 也将自动保存。
createConnection(options)
.then(async connection => {
// 创建 photo 对象
let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.isPublished = true;
// 创建 photo metadata 对象
let metadata = new PhotoMetadata();
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "cybershoot";
metadata.orientation = "portait";
photo.metadata = metadata; // this way we connect them
// 获取 repository
let photoRepository = connection.getRepository(Photo);
// 保存photo的同时保存metadata
await photoRepository.save(photo);
console.log("Photo is saved, photo metadata is saved too.");
})
.catch(error => console.log(error));
查询 新增 删除 编辑 方法
1、查询(find findOne…………)
// 这是****service.ts 文件
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { ProjectManageEntity } from '../entities/projectManage.entity';
import { WorkItemEntity } from '../entities/workItem.entity';
import { HalfMonthEntity } from '../entities/halfMonth.entity';
@Injectable()
export class ProjectManageService {
constructor(
@InjectRepository(UserEntity)
private readonly UserRepository: Repository<any>,
@InjectRepository(ProjectManageEntity)
private readonly projectRepository: Repository<any>,
@InjectRepository(WorkItemEntity)
private readonly workItemRepository: Repository<any>,
@InjectRepository(HalfMonthEntity)
private readonly halfMonthRepository: Repository<any>,
) {}
async queryWay('这里存放是controller传递过来的参数') {
//01
let data = await this.projectRepository
.createQueryBuilder('projectInfo')//名字随便取
.where('projectInfo.leadId = :leadId', {
leadId: '传递过来的参数',
})
.getMany();
//02 findOne
const leader = this.UserRepository.findOne({ id: '传递过来的参数' });
const user = await this.projectRepository.findOne('传递过来的参数');
//03 find
let where = {status:0}//条件
const list = await this.projectRepository.find();
const list = await this.projectRepository.find({
relations: ['leader'],
where,
order: {createTime: 'ASC',},
});
const where = {
halfMonthName: '传递过来的参数',
projectInfo: { leader: { id: '传递过来的参数' } },
};
// 04 通过relations连表查询
const list = await this.halfMonthRepository.find({
relations: ['workItemInfo', 'projectInfo', 'projectInfo.leader', 'workItemInfo.userInfo'],
where,
order: {
projectInfo: 'ASC',
},
});
//05 QueryBuilder联表查询
const list = await this.halfMonthRepository
.createQueryBuilder('halfMonthQuery') //名字随便取
.leftJoinAndSelect('halfMonthQuery.workItemInfo', 'workItemInfo')
.leftJoinAndSelect('halfMonthQuery.projectInfo', 'projectInfo')
.where('halfMonthQuery.halfMonthName = :halfMonthName1 and workItemInfo.userId = :authId1', {
halfMonthName1: '传递过来的参数',
authId1:'传递过来的参数',
})
.getMany();
}
}
2、新增(create save)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { Repository } from 'typeorm';
export class UserService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
//创建
async addUser(createUser) {
// create 只是将数据转化为Entity的实例而已,如果有多余的字段,会在create之后返回的实例中去除
//下面的create也可以省略
const newObj = await this.userRepository.create({
...createParam,
leader,
haha: '我是多余字段',
});
// newObj 中会去除haha字段
const res = await this.userRepository.save({ ...createUser, leader });
}
}
3、删除(delete)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { Not, Repository } from 'typeorm';
export class UserService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
async delUser(id) {
return this.userRepository.delete({
id: id,
role: Not('super'), // role != 'super'
});
}
}
4、编辑(update)
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { ProjectManageEntity } from '../entities/projectManage.entity';
@Injectable()
export class ProjectManageService {
constructor(
private readonly UserRepository: Repository<any>,
@InjectRepository(ProjectManageEntity)
) {}
async addProject(createParam) {
if ('id' in createParam) {
const updateRes = await this.projectRepository.update(
{
id: createParam.id,
},
{
projectName: createParam.projectName,
projectNum: createParam.projectNum,
projectMilestone: createParam.projectMilestone,
devotionTime: createParam.devotionTime,
},
);
return updateRes;
}
}
}
TypeOrm下树形结构(Tree)实体类创建与查询
项目中经常会有组织机构等树形结构的递归场景,数据接口如下:
[{ "id": 1, "name": "a1", "children": [{ "id": 2, "name": "a11", "children": [{ "id": 4, "name": "a111" }, { "id": 5, "name": "a112" }]
}, {
"id": 3,
"name": "a12"
}]
}]
TypeORM 给我们提供了更加便捷的方式来进行存储和查询,官网链接: typeorm.bootcss.com/tree-entiti…
实体定义案例如下:
import {
Column,
Entity,
PrimaryGeneratedColumn,
OneToMany,
Tree,
TreeChildren,
TreeParent,
} from 'typeorm';
import { UserEntity } from './user.entity';
import { ProjectManageEntity } from './projectManage.entity';
@Entity('tbl_department')
@Tree('closure-table') // 数据库存储方式,还可支持其他模式存储
export class DepartmentEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
name: 'depart_name',
length: 120,
nullable: false,
comment: '部门名称',
})
departName: string;
@Column({
type: 'varchar',
name: 'short_name',
length: 80,
comment: '部门简称',
})
shortName: string;
@TreeChildren()
children: DepartmentEntity[]; // 定义children
@TreeParent()
parent: DepartmentEntity; // 定义父级
@OneToMany(() => UserEntity, (m) => m.department)
userList: UserEntity[];
@OneToMany(() => ProjectManageEntity, (m) => m.department)
projectList: ProjectManageEntity[];
}
service.ts 中使用
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TreeRepository } from 'typeorm';
import { DepartmentEntity } from '../entities/department.entity';
@Injectable()
export class DepartmentService {
constructor(
@InjectRepository(DepartmentEntity)
private readonly departmentRepository: TreeRepository<DepartmentEntity>,
) {}
// 整个组织机构树
async departmentTreeQuery() {
return await this.departmentRepository.findTrees();
}
// 当前组织以及相关用户和项目
async findDepartRelations(departId) {
return await this.departmentRepository.findOne(
{ id: departId },
{
relations: ['userList', 'projectList'],
},
);
}
// 当前组织和后代组织
async findDescendantsTree(department) {
return await this.departmentRepository.findDescendantsTree(department);
}
// 当前组织和后代组织,包含关联用户和项目
async findDescendantsTreeRelations() {
return await this.departmentRepository.findTrees({
relations: ['userList', 'projectList'],
});
}
async addDepartment(params) {
const { parentId } = params;
let parent;
if (parentId) {
parent = await this.departmentRepository.findOne(parentId);
}
return await this.departmentRepository.save({ ...params, parent });
}
async editDepartment(id, depart) {
return await this.departmentRepository.update({ id }, depart);
}
async delDepartment(id) {
return await this.departmentRepository.delete({ id });
}
}
几个常用api方法:
- findTrees - 返回数据库中所有树,包括所有子项,子项的子项等。
- findRoots - 根节点是没有祖先的实体。 找到所有根节点但不加载子节点。
- findDescendants - 获取给定实体的所有子项(后代)。 将它们全部返回到数组中。
其他可查看官网链接。
手动事务管理
import { TreeRepository, getConnection, getManager } from 'typeorm';
// 手动事务管理与手写sql
await getManager().transaction(async (transactionalEntityManager) => {
// 内部只能使用回调中的transactionalEntityManager
const depart = await transactionalEntityManager.insert(DepartmentEntity, params);
const id = depart.raw[0].id;
if (!id) {
console.log('初始化组织机构失败', depart);
throw new Error(`初始化组织机构失败`);
}
// 手写sql
await transactionalEntityManager.query(
`INSERT INTO "tbl_department_closure" ("id_ancestor", "id_descendant") VALUES (${id}, ${id})`,
);
});
使用注解实现事务(TODO)
手写sql语句实现查询
有的时候Typeorm提供的方法满足不了我们的查询需要,可以自己写sql语句实现,如下:
queryProjectSumListByUser(halfMonth, userIdList) {
const userIds = userIdList.join("','"); // 注意字符串被拼接进去之后需要有单引号
const entityManager = getManager(); // 手写sql
return entityManager.query(
`select "projectId", SUM("workDays") as "projectSumDays" from "workItem" W where half_month = '${halfMonth}' and "userId" IN ('${userIds}') GROUP BY "projectId"`,
);
}
HttpException异常情况
异常情况在main.ts中处理HttpException异常
import { HttpExceptionFilter } from './core/filter/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
//过滤处理 HTTP 异常
app.useGlobalFilters(new HttpExceptionFilter());
... ...
}
http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
const exceptionResponse = exception.getResponse();
// 设置错误信息
const errorResponse = {
data: {},
msg: exceptionResponse?.message || exception.message,
code: exceptionResponse?.statusCode || status,
};
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
多环境项目配置
需求:项目中数据库、运行端口、初始化数据等均需要在配置文件中约定,且需要在开发环境
测试环境和生产环境等多个环境实现切换
安装
npm i --save @nestjs/config
添加配置
在项目src文件夹下新增一个config文件夹,接着在在config文件夹下,新增 development.ts(开发环境) production.ts(线上环境)index.ts 这3个文件
//development.ts
export default {
//端口
port: parseInt(process.env.PORT, 10) || 3000,
//数据库配置
DATABASE_CONFIG: {
type: 'postgres', // 数据库类型
entities: ['dist/**/*.entity{.ts,.js}'], // 数据表实体
host: '192.168.78.183', // 主机,默认为localhost
port: 5433, // 端口号
username: 'postgres', // 用户名
password: '*********', // 密码
database: 'semiMonthlyReportDev', //数据库名
timezone: '+08:00', //服务器上配置的时区
synchronize: false, //根据实体自动创建数据库表, 生产环境建议关闭
logging: false,
},
};
//production.ts
export default {
//端口
port: parseInt(process.env.PORT, 10) || 3000,
//数据库配置
DATABASE_CONFIG: {
type: 'postgres', // 数据库类型
entities: ['dist/**/*.entity{.ts,.js}'], // 数据表实体
host: '192.168.78.183', // 主机,默认为localhost
port: 5433, // 端口号
username: 'postgres', // 用户名
password: '*********', // 密码
database: 'semiMonthlyReport', //数据库名
timezone: '+08:00', //服务器上配置的时区
synchronize: false, //根据实体自动创建数据库表, 生产环境建议关闭
logging: false,
},
};
//index.ts
import developmentConfig from './development';
import productionConfig from './production';
const configs = {
development: developmentConfig,
production: productionConfig,
};
const env = process.env.NODE_ENV || 'development';
console.log('现在的环境是', env);
export default () => configs[env];
使用
//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import { UserModule } from './user/user.module';
import { ProjectManageModule } from './project-manage/project-manage.module';
import customConfig from './config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 设置为全局
load: [customConfig], //加载自定义配置
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService], //注入服务
useFactory: (configService: ConfigService) => configService.get('DATABASE_CONFIG'),
}),
UserModule,
ProjectManageModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
JWT 实现登录
JWT 登录流程:
- 客户端用户进行登录请求;
- 服务端拿到请求,根据参数查询用户表;
- 若匹配到用户,将用户信息进行签证,并颁发 Token;
- 客户端拿到 Token 后,存储至某一地方,在之后的请求中都带上 Token ;
- 服务端接收到带 Token 的请求后,直接根据签证进行校验,无需再查询用户信息;
实现方式
下面我们从接口的流程从前到后说明
依赖
npm i passport passport-jwt @nestjs/passport @nestjs/jwt
(1)先从接口进入
@Post('login')
// 如果使用@Response()需要使用res.send返回接口信息,使用原来的return无法返回响应
async login(@Body() info: LoginDto, @Response() res) {
try {
const { account, pwd } = info;
const person = await this.userService.findUser(account);
// 登录失败
if (person.pwd !== pwd) {
throw new Error('账号或密码错误');
}
//登录成功
// 签发token: 此处将用户id和所属的组织机构id(后面接口会根据组织机构id做数据筛选)作为参数生成token
let tokenParams = {
userId: person.id,
};
// 按需添加departId属性:有的用户不属于任何组织机构,则不加
if (person.department) {
tokenParams['departId'] = person.department.id;
}
const token = this.certificationService.genToken(tokenParams);
// 如通过Cookie来做也可以
// res.cookie('auth', token, {
// httpOnly: true, // 前端不可获取和修改cookie,只能在http请求中自行携带
// signed: true,
// });
res.json(
ResultData.success({
account: person.account,
userName: person.userName,
role: person.role,
userProjectCount: person.userProjectInfo?.length,
auth: token.accessToken,
}),
);
} catch (err) {
res.json(ResultData.fail(AppHttpCode.LOGIN_FAIL, err.message));
}
}
(2)生成、校验、解析token
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class CertificationService {
public constructor(
@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
) {}
/**
* 生成 token 与 刷新 token
* @param payload
* @returns
*/
genToken(payload: { userId: string; departId?: number }) {
const accessToken = `Bearer ${this.jwtService.sign(payload)}`;
const refreshToken = this.jwtService.sign(payload, {
expiresIn: this.configService.get('JWT.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
/**
* 生成刷新 token
*/
refreshToken(id: string): string {
return this.jwtService.sign({ id });
}
/** 校验 token */
async verifyToken(token: string) {
try {
if (!token) return null;
const id = await this.jwtService.verify(token.replace('Bearer ', ''));
return id;
} catch (error) {
return null;
}
}
}
(3)jwt初始化类配置
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
/**
* 这里的构造函数向父类传递了授权时必要的参数,在实例化时,父类会得知授权时,客户端的请求必须使用 Authorization 作为请求头,
* 而这个请求头的内容前缀也必须为 Bearer,在解码授权令牌时,使用秘钥 secretOrKey: 'secretKey' 来将授权令牌解码为创建令牌时的 payload。
*/
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('JWT.secretkey'),
});
}
/**
* validate 方法实现了父类的抽象方法,在解密授权令牌成功后,即本次请求的授权令牌是没有过期的,
* 此时会将解密后的 payload 作为参数传递给 validate 方法,这个方法需要做具体的授权逻辑,比如这里我使用了通过用户名查找用户是否存在。
* 当用户不存在时,说明令牌有误,可能是被伪造了,此时需抛出 UnauthorizedException 未授权异常。
* 当用户存在时,会将 user 对象添加到 req 中,在之后的 req 对象中,可以使用 req.user 获取当前登录用户。
*/
async validate(payload: any) {
console.log(`JWT验证 - Step 4: 被守卫调用`);
const info = payload.info;
//注意:为了防止jwt被编译出来,所以采用了加密方式,
//注释掉的部份只是为了验证,可以不编写,如果有其他需求的话按该方式进行解码
// const userInfo = crypto.AES.decrypt(info, 'salt').toString(crypto.enc.Utf8);
// console.log(JSON.parse(userInfo));
//return的信息会被放在request里的user部份,可以通过 ctx.switchToHttp().getRequest().user进行获取
return {
info,
};
}
}
(4) module注入
certification.module.ts
import { Module } from '@nestjs/common';
import { CertificationController } from './certification.controller';
import { CertificationService } from './certification.service';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
secret: config.get('JWT.secretkey'),
signOptions: {
expiresIn: config.get('JWT.expiresin'),
},
}),
inject: [ConfigService],
}),
],
controllers: [CertificationController],
providers: [CertificationService, JwtStrategy],
exports: [CertificationService],
})
export class CertificationModule {}
(5) Guard守卫校验token
auth.guard.ts
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import {
ExecutionContext,
ForbiddenException,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CheckTokenFlag } from '../decorators/notCheckToken.decorator';
import { CertificationService } from '../certification/certification.service';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly reflector: Reflector,
@Inject(CertificationService)
private readonly certificationService: CertificationService,
) {
super();
}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
// const res = ctx.switchToHttp().getResponse()
const accessToken = req.get('Auth');
if (!accessToken) throw new ForbiddenException('请先登录');
const { userId, departId } = await this.certificationService.verifyToken(accessToken);
if (!userId) throw new UnauthorizedException('当前登录已过期,请重新登录');
return true;
}
async activate(ctx: ExecutionContext): Promise<boolean> {
return super.canActivate(ctx) as Promise<boolean>;
}
}
绑定到 Module 上后,我们可以在 controller 上使用守卫装饰器 @UseGuards(AuthGuard) 验证是否生效, demo 如下
import { JwtAuthGuard } from '../guards/auth.guard';
//查询
@Get('list')
@UseGuards(JwtAuthGuard) // 守卫只会控制这个方法
async findAll(@Headers() headers) {
try {
console.log('Header', headers.auth, headers.jsc);
const list = await this.userService.findAll();
return ResultData.success(list);
} catch ({ message }) {
return ResultData.fail(AppHttpCode.COMMON_ERR, message);
}
}
But 缺点
这种方式有一个缺点是需要在每个 controller 都需要写上 @UseGuards(AuthGuard)
下面我们使用app的全局守卫来修改一下
// app.module.ts
@Module({
...
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard
}
]
}
有的接口不校验怎么办
当我们使用全局守卫时,所有接口请求头都必须带上 token ,而实际情况项目有的接口是不需要 token 的,如 登录、注册等,那我们必须提供一种机制来将接口路由不校验 token
编写自定义装饰器
// 自定义装饰器:提供一种机制来将某些有该注解的接口不校验 token
import { SetMetadata } from '@nestjs/common';
export const CheckTokenFlag = false;
/**
* 允许 接口 不校验 token
*/
export const NotCheckToken = () => SetMetadata(CheckTokenFlag, true); // 将CheckTokenFlag 置为 true
修改auth.guard.ts
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import {
ExecutionContext,
ForbiddenException,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CheckTokenFlag } from '../decorators/notCheckToken.decorator';
import { CertificationService } from '../certification/certification.service';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly reflector: Reflector,
@Inject(CertificationService)
private readonly certificationService: CertificationService,
) {
super();
}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
// !!!重点:::是否允许 无 token 访问;获取到具有@NotCheckToken() 注解的接口方法的值
const pass = this.reflector.getAllAndOverride<boolean>(CheckTokenFlag, [
ctx.getHandler(),
ctx.getClass(),
]);
if (pass) return true;
const req = ctx.switchToHttp().getRequest();
// const res = ctx.switchToHttp().getResponse()
const accessToken = req.get('Auth');
if (!accessToken) throw new ForbiddenException('请先登录');
const { userId, departId } = await this.certificationService.verifyToken(accessToken);
if (!userId) throw new UnauthorizedException('当前登录已过期,请重新登录');
return true;
}
async activate(ctx: ExecutionContext): Promise<boolean> {
return super.canActivate(ctx) as Promise<boolean>;
}
}
装饰器使用案例
import { NotCheckToken } from '../decorators/notCheckToken.decorator';
//查询
@Get('queryList')
@NotCheckToken()
async findAll(@Query() query) {
try {
const weekdays = this.configService.get('WEEKDAYS');
query['weekdays'] = weekdays;
const data = await this.projectManageService.findAll(query);
return ResultData.success(data);
} catch (err) {
return ResultData.fail(AppHttpCode.COMMON_ERR, err?.message);
}
}
总结:
到此我们就实现了jwt的登录与全局token守卫校验,同时也支持了部分接口不需要校验的自定义注解方式!!!简直完美
文件上传与下载
创建 album模块
nest g mo album //创建 album 模块
nest g co album //创建控制器
nest g service album //创建服务类
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigService } from 'nestjs-config';
import { AlbumController } from './album.controller';
import { AlbumService } from './album.service';
@Module({
imports: [
MulterModule.registerAsync({
useFactory: (config: ConfigService) => config.get('file'),
inject: [ConfigService],
}),
],
controllers: [AlbumController],
providers: [AlbumService],
})
export class AlbumModule {}
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
Get,
Res,
} from '@nestjs/common';
import { Response } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { AlbumService } from './album.service';
@Controller('album')
export class AlbumController {
constructor(private readonly albumService: AlbumService) {}
@Post()
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile() file) {
this.albumService.upload(file);
return true;
}
@Get('export')
async downloadAll(@Res() res: Response) {
const { filename, tarStream } = await this.albumService.downloadAll();
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader(
'Content-Disposition',
`attachment; filename=${filename}`,
);
tarStream.pipe(res);
}
}
import { Injectable } from '@nestjs/common';
import { tar } from 'compressing';
import { ConfigService } from 'nestjs-config';
@Injectable()
export class AlbumService {
constructor(private readonly configService: ConfigService) {}
//上传
upload(file) {
console.log(file);
}
//下载
async downloadAll() {
const uploadDir = this.configService.get('file').root;
const tarStream = new tar.Stream();
await tarStream.addEntry(uploadDir);
return { filename: 'hello-world.tar', tarStream };
}
}
附录
依赖注入与控制反转的概念解释
依赖注入DI和控制反转IOC
• IoC——Inversion of Control 控制反转
• DI——Dependency Injection 依赖注入
要想理解上面两个概念,就必须搞清楚如下的问题:
• 参与者都有谁?
• 依赖:谁依赖于谁?为什么需要依赖?
• 注入:谁注入于谁?到底注入什么?
• 控制反转:谁控制谁?控制什么?为何叫反转(有反转就应该有正转了)?
• 依赖注入和控制反转是同一概念吗?
下面就来简要的回答一下上述问题,把这些问题搞明白了,IoC/DI也就明白了。
参与者都有谁
一般有三方参与者:一个是某个对象;一个是IoC/DI的容器;另一个是某个对象的外部资源。
- 某个对象指的就是任意的、普通的Java对象;
- IoC/DI的容器简单点说就是指用来实现IoC/DI功能的一个框架程序;
- 对象的外部资源指的就是对象需要的,但是是从对象外部获取的,都统称资源,比如:对象需要的其它对象、或者是对象需要的文件资源等等。
谁依赖于谁,为什么需要依赖
当然是某个对象依赖于IoC/DI的容器
对象需要IoC/DI的容器来提供对象需要的外部资源
谁注入于谁,到底注入什么
很明显是IoC/DI的容器 注入 某个对象
就是注入某个对象所需要的外部资源
谁控制谁,控制什么
当然是IoC/DI的容器来控制对象了
主要是控制对象实例的创建
为何叫反转
反转是相对于正转而言的,那么什么算是正转的呢?
考虑一下常规情况下的应用程序,如果要在A里面使用C,你会怎么做呢?当然是直接去创建C的对象,也就是说,是在A类中主动去获取所需要的外部资源C,这种情况被称为正转的。
那么什么是反向呢?就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中。
用图例来说明一下,先看没有IoC/DI的时候,常规的A类使用C类的示意图,如图所示:
当有了IoC/DI的容器后,A类不再主动去创建C了,如图所示:
而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中,如图所示:
依赖注入和控制反转是同一概念吗?
\
根据上面的讲述,应该能看出来,依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。依赖注入是从应用程序的角度在描述,应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
\
小结一下
其实IoC/DI对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC/DI容器来创建并注入它所需要的资源了。
这么小小的一个改变其实是编程思想的一个大进步,这样就有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
\
\
dto常用注解
// 验证非空字符串
@IsString()
@IsNotEmpty()
public name: string;
// 仅在它是请求正文的一部分时才进行验证
@IsString()
@IsNotEmpty()
@IsOptional()
public email: string;
// 验证整数
@IsNumber()
public age: number;
// 验证整数
@IsBoolean()
public acceptedTOS: boolean;
// 验证非空整数数组
@IsArray()
@IsNumber({ allowNaN: false }, { each: true })
@ArrayMinSize(1)
public nums: number[];
优秀开源项目
参考文章
干货!一篇能带你搞懂前端Nest.js核心原理的文章 (juejin.cn/post/711864…
依赖注入和控制反转(www.iteye.com/blog/baitai…)