1、前言
本文章记录自己学习
Nest的过程,适于前端及对后端没有基础但对Nest感兴趣的同学,如有错误,欢迎各位大佬指正
2、Nest成长足迹系列:
- 篇幅一: Nest介绍、swagger的使用
- 篇幅二:TypeORM操作数据库、Pipe校验参数
- 篇幅三:用户登录颁发jwt(本篇)
- 篇幅四:使用nodemailer发送html/ejs文件模版的邮箱验证码
- 篇幅五:如何上传文件
3、前文回顾
- 在第一篇文章已经初步介绍了
Nest以及其swagger文档的使用,第二篇开始讲述Nest如何连接数据库、在创建实体中使用Pipe管道进行参数校验 - 那么本文开始讲述在
Nest中用户登录颁发jwt、校验jwt,本地鉴权
4、项目目录
Nestjs一开始的目录结构是这样的:使用命令nest g res user,会直接生成一个user模块,像controller、service都集成到一起了,与我之前的习惯不同,我还是更喜欢将controller、service抽到一个单独的文件夹中,舍弃每个模块的module,只保留一个app.module.ts(当然了,后续也可按情况保留其他模块的module),没有module的模块就直接在app.module.ts文件中进行注入- 以下是我个人的项目目录
auth是授权相关内容common是通用模块,我一般放一些异常过滤器和统一全局响应输出controller就是控制层了(包含路由)dto用于定义系统内的接口或者输入和输出,方便在swagger上进行测试使用entities数据库实体middleware中间件services服务层utils工具类函数
5、用户注册、登录,颁发Jwt
5.1、创建用户实体
首先我们需要创建一个用户实体,用于存储用户的相关信息,例如用户名、密码、角色等等。在NestJS项目中创建一个 user.entity.ts 文件,内容如下:
import {
Entity,
Column,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn
} from 'typeorm';
import { Exclude } from 'class-transformer';
import { Role } from './role.entity';
// @Entity()装饰器自动从所有类生成一个SQL表,以及他们包含的元数据
// @Entity('users') // sql表名为users
@Entity() // sql表名为user
export class User {
// 主键装饰器,也会进行自增
@PrimaryGeneratedColumn()
id: number;
// 列装饰器
@Column()
username: string;
// 请求返回数据时将密码这个字段隐藏
@Exclude()
@Column()
password: string;
// 定义与其他表的关系
// name 用于指定创中间表的表名
// JoinTable只在关系一边
@JoinTable({ name: 'user_roles' })
// 指定多对多关系
/**
* 关系类型,返回相关实体引用
* cascade: true,插入和更新启用级联,也可设置为仅插入或仅更新
* ['insert']
*/
@ManyToMany(() => Role, (role) => role.users, { cascade: true })
roles: Role[];
@CreateDateColumn()
createAt: Date;
@UpdateDateColumn()
updateAt: Date;
}
如果对实体中一些字段不清楚,可以翻看这个系列的第二篇文章:篇幅二:TypeORM操作数据库、Pipe校验参数
5.2、创建user模块,实现注册等接口
-
这个模块包含
user.module.ts、user.controller.ts和user.service.ts三个文件的内容,可以使用Nestjs提供的命令进行创建,可以看本系列的第一篇:篇幅一: Nest介绍、swagger的使用,这部分模块有以下几个接口:- 用户注册、获取用户列表、根据用户
id获取用户信息、更新用户数据、删除用户数据
- 用户注册、获取用户列表、根据用户
-
user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from '@/services/user.service';
import { UserController } from '@/controllers/user.controller';
import { User } from '@/entities/user.entity';
import { Role } from '@/entities/role.entity';
@Module({
imports: [TypeOrmModule.forFeature([User, Role])],
controllers: [UserController],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
user.controller.ts- 在这个文件中,像
UseGuards、AuthGuard、JwtAuthGuard都是在认证颁发jwt和角色权限校验中需要使用到的
- 在这个文件中,像
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
UseInterceptors,
ClassSerializerInterceptor,
UseGuards
} from '@nestjs/common';
import { ApiOperation, ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from '@/services/user.service';
import { CreateUserDto } from '@/dto/user/create-user.dto';
import { UpdateUserDto } from '@/dto/user/update-user.dto';
import { PaginationQueryDto } from '@/common/dto/pagination-query.dto';
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
import { BUSINESS_ERROR_CODE } from '@/common/exceptions/business.error.codes';
import { BusinessException } from '@/common/exceptions/business.exception';
// 设置swagger文档标签分类
@ApiTags('用户模块')
// 使用装饰器修饰类(路由)
@Controller('user')
export class UserController {
// 依赖注入的方式,引入service
constructor(private readonly userService: UserService) {}
@Post('create')
@ApiOperation({
summary: '添加用户' // 接口描述信息
})
// 请求返回数据时将密码这个字段隐藏
@UseInterceptors(ClassSerializerInterceptor)
// @Body是指获取到(http请求)客户端传递过来的body体中的数据,将数据给createUserDto这个变量,CreateUserDto是TS类型约束
// createUserDto可自定义
create(@Body() createUserDto: CreateUserDto) {
try {
return this.userService.create(createUserDto);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '用户添加失败'
});
}
}
@Get('list')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard) // 验证token
// @UseGuards(AuthGuard('jwt'))
@ApiOperation({
summary: '获取user列表'
})
findAll(@Query() paginationsQuery: PaginationQueryDto) {
console.log(paginationsQuery, '5-进行接口请求');
try {
return this.userService.getUserList(paginationsQuery);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '获取用户列表失败'
});
}
}
@Get('list/:id')
@ApiOperation({
summary: '根据id获取user'
})
findOne(@Param('id') id: string) {
try {
return this.userService.findOneById(+id);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '获取用户失败'
});
}
}
@Patch('list/:id')
@ApiOperation({
summary: '根据id修改user'
})
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
try {
return this.userService.update(+id, updateUserDto);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '修改用户失败'
});
}
}
@Delete('list/:id')
@ApiOperation({
summary: '根据id删除user'
})
remove(@Param('id') id: string) {
try {
return this.userService.remove(+id);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '删除用户失败'
});
}
}
}
user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreateUserDto } from '@/dto/user/create-user.dto';
import { UpdateUserDto } from '@/dto/user/update-user.dto';
import { User } from '@/entities/user.entity';
import { Role } from '@/entities/role.entity';
import { PaginationQueryDto } from '@/common/dto/pagination-query.dto';
import { BUSINESS_ERROR_CODE } from '@/common/exceptions/business.error.codes';
import { BusinessException } from '@/common/exceptions/business.exception';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>
) {}
async create(createUserDto: CreateUserDto) {
// const user = await this.userRepository.create({ ...createUserDto });
// return this.userRepository.save(user);
try {
const roles = await Promise.all(
createUserDto.roles.map((name) => this.preloadRoleByName(name))
);
const user = this.userRepository.create({ ...createUserDto, roles });
return this.userRepository.save(user);
} catch (error) {
console.log(error);
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '用户添加失败'
});
}
}
async getUserList(paginationsQuery: PaginationQueryDto) {
const { limit, offset } = paginationsQuery;
return await this.userRepository.find({
// 新的方式,指定关系才能查到roles
// 1
relations: ['roles'],
// relations: {
// roles: true
// },
skip: offset,
take: limit
});
}
async findOneById(id: number) {
return await this.userRepository.findOne({
where: { id },
relations: { roles: true }
});
}
async findOneByUserName(username: string) {
return await this.userRepository.findOne({
where: { username },
relations: { roles: true }
});
}
async update(id: number, updateUserDto: UpdateUserDto) {
// 因为更新的每个项都是可选的,所以需要确保role一定存在
try {
const roles =
updateUserDto.roles &&
(await Promise.all(
updateUserDto.roles.map((name) => this.preloadRoleByName(name))
));
const user = await this.userRepository.preload({
id: id,
...updateUserDto,
roles
});
if (!user) {
throw new NotFoundException(`${id} not found`);
}
return this.userRepository.save(user);
} catch (error) {
console.log(error);
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '用户修改失败'
});
}
}
async remove(id: number) {
const user = await this.userRepository.findOneBy({ id });
if (!user) {
throw new NotFoundException(`${id} not found`);
}
return await this.userRepository.remove(user);
}
// 私有方法,将角色名作为入参并返回
private async preloadRoleByName(name: string): Promise<Role> {
const existingRole = await this.roleRepository.findOne({ where: { name } });
if (existingRole) {
return existingRole;
}
return this.roleRepository.create({ name });
}
}
5.3 用户登录/认证、颁发JWT
创建auth授权模块,用户登录接口是可以写在用户模块的,但我把它归为授权的一类,所以写到了授权模块,点击查看nestjs中文文档认证模块
5.3.1首先安装依赖包
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
npm install @nestjs/jwt passport-jwt
npm install @types/passport-jwt --save-dev
5.3.2创建auth模块
- 这个模块包含
auth.controller.ts、auth.service.ts、auth.module.ts三大块 auth.controller.ts- 在这有一个登录路由,并且
@UseGuards(LocalAuthGuard)启用了本地身份验证,登录时会调用auth.service.ts中的login方法
- 在这有一个登录路由,并且
import {
Controller,
Post,
UseGuards,
Body
} from '@nestjs/common';
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { LocalAuthGuard } from '@/auth/guards/local-auth.guard';
import { AuthService } from '@/services/auth.service';
import { LoginUserDto } from '@/dto/user/Login-user.dto';
import { BUSINESS_ERROR_CODE } from '@/common/exceptions/business.error.codes';
import { BusinessException } from '@/common/exceptions/business.exception';
@ApiTags('授权模块')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(LocalAuthGuard) // 启用本地身份验证
@ApiBody({ type: LoginUserDto })
@ApiOperation({
summary: '登录'
})
async login(@Body() loginBody: LoginUserDto) {
console.log('2-请求登陆', loginBody);
try {
return await this.authService.login(loginBody);
} catch (error) {
throw new BusinessException({
code: BUSINESS_ERROR_CODE.COMMON,
message: '登录失败'
});
}
}
}
auth.service.ts主要有以下几个功能- 通过用户
id从数据库中查找这个用户的信息 - 验证用户是否存在
- 处理
jwt签证,使用了@nestjs/jwt提供的sign()函数,用于从用户对象属性的子集生成jwt - 校验
token
- 通过用户
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from '@/services/user.service';
import { md5password } from '@/utils/password-handle';
import { BusinessException } from '@/common/exceptions/business.exception';
import { LoginUserDto } from '@/dto/user/login-user.dto';
import { User } from '@/entities/user.entity';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly jwtService: JwtService
) {}
async findOneById(id: number) {
return await this.userRepository.findOne({
where: { id },
relations: { roles: true }
});
}
// 验证用户是否存在
async validateUser(username: string, password: string): Promise<any> {
const user = await this.userService.findOneByUserName(username);
if (user && user.password === md5password(password)) {
return user;
}
throw new BusinessException('用户名或密码错误');
}
async checkLogin(loginBody: LoginUserDto) {
const { username, password } = loginBody;
const user = await this.userRepository.findOne({
where: { username },
relations: { roles: true }
});
if (user && user.password === md5password(password)) {
return user;
}
throw new BusinessException('用户名或密码错误');
}
// 处理jwt签证
async login(user: any) {
console.log('3-处理jwt签证');
const result = await this.validateUser(user.username, user.password);
const payload = {
id: result.id,
username: result.username,
roles: result.roles
};
return {
message: '登录成功!',
username: user.username,
token: this.jwtService.sign(payload)
};
}
// 校验token
async verifyToken(token: string) {
if (token) {
const jwt = token.replace('Bearer', '');
const id = this.jwtService.verify(jwt);
return id;
}
throw new BusinessException('token不存在!');
}
}
auth.module.tssecret: process.env.JWT_KEY表示设置密钥(从环境变量中获取JWT_KEY作为密钥)signOptions: { expiresIn: '8h' }表示这个JWT八个小时后过期
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthService } from '@/services/auth.service';
import { AuthController } from '@/controllers/auth.controller';
import { UserModule } from '@/modules/user.module';
import { LocalStrategy } from '@/auth/strategies/local.strategy';
import { JwtStrategy } from '@/auth/strategies/jwt.strategy';
import { User } from '@/entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true
}),
PassportModule,
JwtModule.register({
secret: process.env.JWT_KEY,
signOptions: { expiresIn: '8h' }
}),
TypeOrmModule.forFeature([User]),
UserModule
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService]
})
export class AuthModule {}
5.3.3实现本地身份验证策略
- 在上述
auth.module.ts文件中出现了LocalStrategy和JwtStrategy,这两分别是本地验证策略和JWT验证策略 - 在
auth文件夹中创建一个名为local.strategy.ts文件,本地身份验证策略,将在登录前执行 local.strategy.ts
// 实现 Passport 本地身份验证策略
// 本地策略主要是验证账号和密码是否存在,如果存在就登陆,返回token
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '@/services/auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
// 登录时进行了本地身份验证
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
console.log('1-登录前进行了本地身份验证');
if (!user) {
throw new UnauthorizedException('账号或密码错误!');
}
return user;
}
}
- 在
auth文件夹中创建一个名为jwt.strategy.ts文件,本地身份验证策略,将在登录前执行jwtFromRequest:提供从请求中提取JWT的方法。我们将使用在API请求的授权头中提供token的标准方法。ignoreExpiration:我们选择默认的false,进行验证token是否过期,它将确保JWT没有过期的责任委托给Passport模块。这意味着,如果我们的路由提供了一个过期的JWT,请求将被拒绝,并发送401未经授权的响应。secretOrKey:我们用来加密JWT的密钥。
jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { AuthService } from '@/services/auth.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, // 为true则不验证token的到期时间
secretOrKey: process.env.JWT_KEY
});
}
// 验证回调
// 对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递。根据 JWT 签名的工作方式,我们可以保证接收到之前已签名并发给有效用户的有效 token 令牌。
async validate(payload: any) {
/**
* payload是用户信息和登录时间以及过期时间
*/
const existUser = await this.authService.findOneById(payload.id);
console.log('4-验证回调');
if (!existUser) {
throw new UnauthorizedException('token不正确');
}
}
}
- 源码地址,在源码中还有一些异常过滤器的使用、中间件的使用、角色权限验证等