配置抽离
yarn add nestjs-config
app.module.ts
import * as path from 'path';
import { Module } from '@nestjs/common';
//数据库
import { TypeOrmModule } from '@nestjs/typeorm';
//全局配置
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
//1.配置config目录
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
//2.读取配置,这里读取的是数据库配置
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService], // 获取服务注入
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
配置数据库
// src -> config -> database
import { join } from 'path';
export default {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'your password',
database: 'test',
entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')],
synchronize: true,
};
环境配置
yarn add cross-env
cross-env的作用是兼容window系统和mac系统来设置环境变量
在package.json中配置
"scripts": {
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
},
dotenv的使用
yarn add dotenv
根目录创建 env.parse.ts
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
const isProd = process.env.NODE_ENV === 'production';
const localEnv = path.resolve('.env.local');
const prodEnv = path.resolve('.env.prod');
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
// 配置 通过process.env.xx读取变量
dotenv.config({ path: filePath });
导入环境
// main.ts
import '../env.parse'; // 导入环境变量
.env.local
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123
MYSQL_DATABASE=test
.env.prod
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=1234
MYSQL_DATABASE=test
读取环境变量 process.env.MYSQL_HOST
形式
文件上传与下载
yarn add @nestjs/platform-express compressing
compressing 文件下载依赖,提供流的方式
配置文件的目录地址,以及文件的名字格式
// src/config/file.ts 上传文件配置
import { join } from 'path';
import { diskStorage } from 'multer';
/**
* 上传文件配置
*/
export default {
root: join(__dirname, '../../assets/uploads'),
storage: diskStorage({
destination: join(
__dirname,
`../../assets/uploads/${new Date().toLocaleDateString()}`,
),
filename: (req, file, cb) => {
const filename = `${new Date().getTime()}.${file.mimetype.split('/')[1]}`;
return cb(null, filename);
},
}),
};
// app.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 加载配置文件目录 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
// upload.controller.ts
import {
Controller,
Get,
Post,
UseInterceptors,
UploadedFile,
UploadedFiles,
Body,
Res,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { FileUploadDto } from './dto/upload-file.dto';
import { UploadService } from './upload.service';
import { Response } from 'express';
@Controller('common')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file) {
this.uploadService.uploadSingleFile(file);
return true;
}
// 多文件上传
@Post('uploads')
@UseInterceptors(FilesInterceptor('file'))
uploadMuliFile(@UploadedFiles() files, @Body() body) {
this.uploadService.UploadMuliFile(files, body);
return true;
}
@Get('export')
async downloadAll(@Res() res: Response) {
const { filename, tarStream } = await this.uploadService.downloadAll();
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
tarStream.pipe(res);
}
}
// upload.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { join } from 'path';
import { createWriteStream } from 'fs';
import { tar } from 'compressing';
import { ConfigService } from 'nestjs-config';
@Injectable()
export class UploadService {
constructor(private readonly configService: ConfigService) {}
uploadSingleFile(file: any) {
console.log('file', file);
}
UploadMuliFile(files: any, body: any) {
console.log('files', files);
}
async downloadAll() {
const uploadDir = this.configService.get('file').root;
const tarStream = new tar.Stream();
await tarStream.addEntry(uploadDir);
return { filename: 'download.tar', tarStream };
}
}
// upload.module.ts
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigService } from 'nestjs-config';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
@Module({
imports: [
MulterModule.registerAsync({
useFactory: (config: ConfigService) => config.get('file'),
inject: [ConfigService],
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
实现图片随机验证码
nest如何实现图片随机验证码?
这里使用的是svg-captcha这个库,你也可以使用其他的库
yarn add svg-captcha
封装,以便多次调用
src -> utils -> tools.service.ts
import { Injectable } from '@nestjs/common';
import * as svgCaptcha from 'svg-captcha';
@Injectable()
export class ToolsService {
async captche(size = 4) {
const captcha = svgCaptcha.create({ //可配置返回的图片信息
size, //生成几个验证码
fontSize: 50, //文字大小
width: 100, //宽度
height: 34, //高度
background: '#cc9966', //背景颜色
});
return captcha;
}
}
在使用的module中引入
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { ToolsService } from '../../utils/tools.service';
@Module({
controllers: [UserController],
providers: [UserService, ToolsService],
})
export class UserModule { }
使用
import { Controller, Get, Post,Body } from '@nestjs/common';
import { EmailService } from './email.service';
@Controller('user')
export class UserController{
constructor(private readonly toolsService: ToolsService,) {} //注入服务
@Get('authcode') //当请求该接口时,返回一张随机图片验证码
async getCode(@Req() req, @Res() res) {
const svgCaptcha = await this.toolsService.captche(); //创建验证码
req.session.code = svgCaptcha.text; //使用session保存验证,用于登陆时验证
console.log(req.session.code);
res.type('image/svg+xml'); //指定返回的类型
res.send(svgCaptcha.data); //给页面返回一张图片
}
@Post('/login')
login(@Body() body, @Session() session) {
//验证验证码,由前端传递过来
const { code } = body;
if(code?.toUpperCase() === session.code?.toUpperCase()){
console.log(‘验证码通过’)
}
return 'hello authcode';
}
}
前端简单代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
form {
display: flex;
}
.input {
width: 80px;
height: 32px;
}
.verify_img {
margin: 0px 5px;
}
</style>
</head>
<body>
<h2>随机验证码</h2>
<form action="/user/login" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name='code' class="input" />
<img class="verify_img" src="/user/code" title="看不清?点击刷新"
onclick="javascript:this.src='/user/code?t='+Math.random()"> //点击再次生成新的验证码
<button type="submit">提交</button>
</form>
</body>
</html>
邮件服务
邮件服务使用文档 nest-modules.github.io/mailer/docs…
// 邮件服务配置
// app.module.ts
import { MailerModule } from '@nestjs-modules/mailer';
import { resolve, join } from 'path';
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 加载配置文件目录 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// 邮件服务配置
MailerModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('email'),
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {}
// src/config/email.ts 邮件服务配置
import { join } from 'path';
// npm i ejs -S
import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter';
export default {
transport: {
host: 'smtp.qq.com',
secureConnection: true, // use SSL
secure: true,
port: 465,
ignoreTLS: false,
auth: {
user: '123@test.com',
pass: 'dfafew1',
},
},
defaults: {
from: '"nestjs" <123@test.com>',
},
// preview: true, // 发送邮件前预览
template: {
dir: join(__dirname, '../templates/email'), // 邮件模板
adapter: new EjsAdapter(),
options: {
strict: true,
},
},
};
邮件服务使用
// email.services.ts
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class EmailService {
// 邮件服务注入
constructor(private mailerService: MailerService) {}
async sendEmail() {
console.log('发送邮件');
await this.mailerService.sendMail({
to: 'test@qq.com', // 收件人
from: '123@test.com', // 发件人
// subject: '副标题',
text: 'welcome', // plaintext body
html: '<h1>hello</h1>', // HTML body content
// template: 'email', // 邮件模板
// context: { // 传入邮件模板的data
// email: 'test@qq.com',
// },
});
return '发送成功';
}
}
nest基于possport + jwt做登陆验证
方式与逻辑
- 基于possport的本地策略和jwt策略
- 本地策略主要是验证账号和密码是否存在,如果存在就登陆,返回token
- jwt策略则是验证用户登陆时附带的token是否匹配和有效,如果不匹配和无效则返回401状态码
yarn add @nestjs/jwt @nestjs/passport passport-jwt passport-local passport
yarn add -D @types/passport @types/passport-jwt @types/passport-local
jwt策略 jwt.strategy.ts
// src/modules/auth/jwt.strategy.ts
import { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromHeader('token'),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret, // 使用密钥解析
} as StrategyOptions);
}
//token验证, payload是super中已经解析好的token信息
async validate(payload: any) {
return { userId: payload.userId, username: payload.username };
}
}
本地策略 local.strategy.ts
// src/modules/auth/local.strategy.ts
import { Strategy, IStrategyOptions } from 'passport-local';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { AuthService } from './auth.service';
//本地策略
//PassportStrategy接受两个参数:
//第一个:Strategy,你要用的策略,这里是passport-local,本地策略
//第二个:别名,可选,默认是passport-local的local,用于接口时传递的字符串
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'username',
passwordField: 'password',
} as IStrategyOptions);
}
// validate是LocalStrategy的内置方法
async validate(username: string, password: string): Promise<any> {
//查询数据库,验证账号密码,并最终返回用户
return await this.authService.validateUser({ username, password });
}
}
constants.ts
// src/modules/auth/constants.ts
export const jwtConstants = {
secret: 'secretKey',
};
使用守卫 auth.controller.ts
// src/modules/auth/auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 登录测试 无需token
@UseGuards(AuthGuard('local')) //本地策略,传递local,执行local里面的validate方法
@Post('login')
async login(@Request() req) { //通过req可以获取到validate方法返回的user,传递给login,登陆
return this.authService.login(req.user);
}
// 在需要的地方使用守卫,需要带token才可访问
@UseGuards(AuthGuard('jwt'))//jwt策略,身份鉴权
@Get('userInfo')
getUserInfo(@Request() req) {//通过req获取到被验证后的user,也可以使用装饰器
return req.user;
}
}
在module引入jwt配置和数据库查询的实体 auth.module.ts
// src/modules/auth/auth.module.ts
import { LocalStrategy } from './local.strategy';
import { jwtConstants } from './constants';
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { UsersEntity } from '../user/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([UsersEntity]),
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '10d' },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
auth.service.ts
// src/modules/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { compareSync } from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersEntity),
private readonly usersRepository: Repository<UsersEntity>,
private jwtService: JwtService
) {}
validateUser(username: string, password: string) {
const user = await this.usersRepository.findOne({
where: { username },
select: ['username', 'password'],
});
if (!user) ToolsService.fail('用户名或密码不正确');
//使用bcryptjs验证密码
if (!compareSync(password, user.password)) {
ToolsService.fail('用户名或密码不正确');
}
return user;
}
login(user: any) {
const payload = { username: user.username }; // 把信息存在token
return {
token: this.jwtService.sign(payload),
};
}
}
最后在app.module.ts中导入即可测试
// app.modules.ts
import { AuthModule } from './modules/auth/auth.module';
@Module({
imports: [
...
AuthModule, // 导入模块
],
controllers: [AppController],
providers: [],
})
export class AppModule implements NestModule {}
使用postman测试
对数据库的密码加密:md5和bcryptjs
密码加密
一般开发中,是不会有人直接将密码明文直接放到数据库当中的。因为这种做法是非常不安全的,需要对密码进行加密处理。
好处:
- 预防内部网站运营人员知道用户的密码
- 预防外部的攻击,尽可能保护用户的隐私
加密方式
- 使用
md5
:每次生成的值是一样的,一些网站可以破解,因为每次存储的都是一样的值 - 使用
bcryptjs
:每次生成的值是不一样的
yarn add md5
加密
import * as md5 from 'md5';
const passwrod = '123456';
const transP = md5(passwrod); // 固定值:e10adc3949ba59abbe56e057f20f883e
给密码加点”盐”:目的是混淆密码,其实还是得到固定的值
const passwrod = '123456';
const salt = 'dmxys'
const transP = md5(passwrod + salt); // 固定值:4e6a2881e83262a72f6c70f48f3e8022
验证密码:先加密,再验证
const passwrod = '123456';
const databasePassword = 'e10adc3949ba59abbe56e057f20f883e'
if (md5(passwrod) === databasePassword ) {
console.log('密码通过');
}
使用bcryptjs
yarn add bcryptjs
yarn add -D @types/bcryptjs
同一密码,每次生成不一样的值
import { compareSync, hashSync } from 'bcryptjs';
const passwrod = '123456';
const transformPass = hashSync(passwrod); $2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS
const transformPass2 = hashSync(passwrod); $2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcW
const transformPass3 = hashSync(passwrod); $2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu
验证密码:使用不同的值 匹配 密码123456,都能通过
const password = '123456';
const databasePassword1 = '$2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS'
const databasePassword2 = '$2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcW'
const databasePassword3 = '$2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu'
if (compareSync(password, databasePassword3)) {
console.log('密码通过');
}
推荐使用bcryptjs
,算法要比md5
高级
角色权限
RBAC
-
- RBAC是基于角色的权限访问控制(Role-Based Access Control)一种数据库设计思想,根据设计数据库设计方案,完成项目的权限管理
- 在RBAC中,有3个基础组成部分,分别是:
用户
、角色
和权限
,权限与角色相关联,用户通过成为适当角色而得到这些角色的权限
- 权限:具备操作某个事务的能力
- 角色:一系列权限的集合
如:一般的管理系统中:
销售人员:仅仅可以查看商品信息
运营人员:可以查看,修改商品信息
管理人员:可以查看,修改,删除,以及修改员工权限等等
管理人员只要为每个员工账号分配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面
权限类型
- 展示(菜单),如:显示用户列表,显示删除按钮等等…
- 操作(功能),如:增删改查,上传下载,发布公告,发起活动等等…
数据库设计
数据库设计:可简单,可复杂,几个人使用的系统和几千人使用的系统是不一样的
小型项目:用户表,权限表
中型项目:用户表,角色表,权限表
大型项目:用户表,用户分组表,角色表,权限表,菜单表…
没有角色的设计
只有用户表,菜单表,两者是多对多关系,有一个关联表
缺点:
- 新建一个用户时,在用户表中添加一条数据
- 新建一个用户时,在关联表中添加N条数据
- 每次新建一个用户需要添加1+N(关联几个)条数据
- 如果有100个用户,每个用户100个权限,那需要添加10000条数据
基于RBAC的设计
用户表和角色表的关系设计:
如果你希望一个用户可以有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系
如果你希望一个用户只能有一个角色,就设计一对多,多对一关系
角色表和权限表的关系设计:
一个角色可以拥有多个权限,一个权限被多个角色使用,设计多对多关系
多对多关系设计
用户表与角色表是多对多关系,角色表与菜单表是多对多关系
更加复杂的设计
实现流程
- 数据表设计
- 实现角色的增删改查
- 实现用户的增删改查,增加和修改用户的时候需要选择角色
- 实现权限的增删改查
- 实现角色与授权的关联
- 判断当前登录的用户是否有访问菜单的权限
- 根据当前登录账户的角色信息动态显示左侧菜单(前端)
代码实现
这里将实现一个用户,部门,角色,权限的例子:
用户通过成为部门的一员,则拥有部门普通角色的权限,还可以单独给用户设置角色,通过角色,获取权限。
权限模块包括,模块,菜单,操作,通过type区分类型,这里就不再拆分。
关系总览:
- 用户 - 部门:一对多关系,这里设计用户只能加入一个部门,如果设计可以加入多个部门,设计为多对多关系
- 用户 - 角色:多对多关系,可以给用户设置多个角色
- 角色 - 部门:多对多关系,一个部门多个角色
- 角色 - 权限:多对多关系,一个角色拥有多个权限,一个权限被多个角色使用
数据库实体设计
用户
import {
Column,
Entity,
ManyToMany,
ManyToOne,
JoinColumn,
JoinTable,
PrimaryGeneratedColumn,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';
@Entity({ name: 'user' })
export class UsersEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
length: 30,
nullable: false,
unique: true,
})
username: string;
@Column({
type: 'varchar',
name: 'password',
length: 100,
nullable: false,
select: false,
comment: '密码',
})
password: string;
@ManyToMany(() => RoleEntity, (role) => role.users)
@JoinTable({ name: 'user_role' })
roles: RoleEntity[];
@ManyToOne(() => DepartmentEntity, (department) => department.users)
@JoinColumn({ name: 'department_id' })
department: DepartmentEntity;
}
角色
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';
import { AccessEntity } from '../../access/entities/access.entity';
@Entity({ name: 'role' })
export class RoleEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 30 })
rolename: string;
@ManyToMany(() => UsersEntity, (user) => user.roles)
users: UsersEntity[];
@ManyToMany(() => DepartmentEntity, (department) => department.roles)
department: DepartmentEntity[];
@ManyToMany(() => AccessEntity, (access) => access.roles)
@JoinTable({ name: 'role_access' })
access: AccessEntity[];
}
部门
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
OneToMany,
JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { RoleEntity } from '../../role/entities/role.entity';
@Entity({ name: 'department' })
export class DepartmentEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 30 })
departmentname: string;
@OneToMany(() => UsersEntity, (user) => user.department)
users: UsersEntity[];
@ManyToMany(() => RoleEntity, (role) => role.department)
@JoinTable({ name: 'department_role' })
roles: RoleEntity[];
}
权限
import {
Entity,
PrimaryGeneratedColumn,
Column,
Tree,
TreeChildren,
TreeParent,
ManyToMany,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';
@Entity({ name: 'access' })
@Tree('closure-table')
export class AccessEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 30, comment: '模块' })
module_name: string;
@Column({ type: 'varchar', length: 30, nullable: true, comment: '操作' })
action_name: string;
@Column({ type: 'tinyint', comment: '类型:1:模块,2:菜单,3:操作' })
type: number;
@Column({ type: 'text', nullable: true, comment: '操作地址' })
url: string;
@TreeParent()
parentCategory: AccessEntity;
@TreeChildren()
childCategorys: AccessEntity[];
@ManyToMany(() => RoleEntity, (role) => role.access)
roles: RoleEntity[];
}
接口实现
由于要实现很多接口,这里只说明一部分,其实都是数据库的操作,所有接口如下:
根据用户的id获取信息:id,用户名,部门名,角色,这些信息在做用户登陆时传递到token中。
这里设计的是:创建用户时,添加部门,就会成为部门的普通角色,也可单独设置角色,但不是每个用户都有单独的角色。
async getUserinfoByUid(uid: number) {
获取用户
const user = await this.usersRepository.findOne(
{ id: uid },
{ relations: ['roles'] },
);
if (!user) ToolsService.fail('用户ID不存在');
const sql = `
select
user.id as user_id, user.username, user.department_id, department.departmentname, role.id as role_id, rolename
from
user, department, role, department_role as dr
where
user.department_id = department.id
and department.id = dr.departmentId
and role.id = dr.roleId
and user.id = ${uid}`;
const result = await this.usersRepository.query(sql);
const userinfo = result[0];
const userObj = {
user_id: userinfo.user_id,
username: userinfo.username,
department_id: userinfo.department_id,
departmentname: userinfo.departmentname,
roles: [{ id: userinfo.role_id, rolename: userinfo.rolename }],
};
// 如果用户的角色roles有值,证明单独设置了角色,所以需要拼接起来
if (user.roles.length > 0) {
const _user = JSON.parse(JSON.stringify(user));
userObj.roles = [...userObj.roles, ..._user.roles];
}
return userObj;
}
// 接口请求结果:
{
"status": 200,
"message": "请求成功",
"data": {
"user_id": 1,
"username": "admin",
"department_id": 1,
"departmentname": "销售部",
"roles": [
{
"id": 1,
"rolename": "销售部员工"
},
{
"id": 5,
"rolename": "admin"
}
]
}
}
结合possport + jwt 做用户登陆授权验证
在验证账户密码通过后,possport 返回用户,然后根据用户id获取用户信息,存储token,用于路由守卫,还可以使用redis存储,以作他用。
async login(user: any): Promise<any> {
const { id } = user;
const userResult = await this.userService.getUserinfoByUid(id);
const access_token = this.jwtService.sign(userResult);
await this.redisService.set(`user-token-${id}`, access_token, 60 * 60 * 24);
return { access_token };
}
{
"status": 200,
"message": "请求成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZGVwYXJ0bWVudF9pZCI6MSwiZGVwYXJ0bWVudG5hbWUiOiLplIDllK7pg6giLCJyb2xlcyI6W3siaWQiOjEsInJvbGVuYW1lIjoi6ZSA5ZSu6YOo5ZGY5belIn0seyJpZCI6NSwicm9sZW5hbWUiOiJhZG1pbiJ9XSwiaWF0IjoxNjIxNjA1Nzg5LCJleHAiOjE2MjE2OTIxODl9.VIp0MdzSPM13eq1Bn8bB9Iu_SLKy4yoMU2N4uwgWDls"
}
}
后端的权限访问
使用守卫,装饰器,结合token,验证访问权限
逻辑:
- 第一步:在
controller
使用自定义守卫装饰接口路径,在请求该接口路径时,全部进入守卫逻辑 - 第二步:使用自定义装饰器装饰特定接口,传递角色,自定义守卫会使用反射器获取该值,以判断该用户是否有权限
如下:findOne
接口使用了自定义装饰器装饰接口,意思是只能admin
来访问
import {
Controller,
Get,
Body,
Patch,
Post,
Param,
Delete,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthGuard } from '../../common/guard/auth.guard';
import { Roles } from '../../common/decorator/role.decorator';
@UseGuards(AuthGuard) // 自定义守卫
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
@Get()
async findAll() {
const [data, count] = await this.userService.findAll();
return { count, data };
}
@Get(':id')
@Roles('admin') // 自定义装饰器
async findOne(@Param('id', new ParseIntPipe()) id: number) {
return await this.userService.findOne(id);
}
}
装饰器
import { SetMetadata } from '@nestjs/common';
// SetMetadata作用:将获取到的值,设置到元数据中,然后守卫通过反射器才能获取到值
export const Roles = (...args: string[]) => SetMetadata('roles', args);
自定义守卫
返回
true
则有访问权限,返回false
则直接报403
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core'; // 反射器,作用与自定义装饰器桥接
import { ToolsService } from '../../utils/tools.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly jwtService: JwtService,
) { }
// 白名单数组
private whiteUrlList: string[] = [];
// 验证该次请求是否为白名单内的路由
private isWhiteUrl(urlList: string[], url: string): boolean {
if (urlList.includes(url)) {
return true;
}
return false;
}
canActivate(context: ExecutionContext): boolean {
// 获取请求对象
const request = context.switchToHttp().getRequest();
// 验证是否是白名单内的路由
if (this.isWhiteUrl(this.whiteUrlList, request.url)) return true;
// 获取请求头中的token字段,解析获取存储在token的用户信息
const token = context.switchToRpc().getData().headers.token;
const user: any = this.jwtService.decode(token);
if (!user) ToolsService.fail('token获取失败,请传递token或书写正确');
// 使用反射器,配合装饰器使用,获取装饰器传递过来的数据
const authRoles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);
// 如果没有使用roles装饰,就获取不到值,就不鉴权,等于白名单
if (!authRoles) return true;
// 如果用户的所属角色与装饰器传递过来的值匹配则通过,否则不通过
const userRoles = user.roles;
for (let i = 0; i < userRoles.length; i++) {
if (authRoles.includes(userRoles[i].rolename)) {
return true;
}
}
return false;
}
}
简单测试
两个用户,分别对应不同的角色,分别请求user的findOne接口
用户1:销售部员工和admin
用户2:人事部员工
用户1:销售部员工和admin
{
"status": 200,
"message": "请求成功",
"data": {
"user_id": 1,
"username": "admin",
"department_id": 1,
"departmentname": "销售部",
"roles": [
{
"id": 1,
"rolename": "销售部员工"
},
{
"id": 5,
"rolename": "admin"
}
]
}
}
用户2:人事部员工
{
"status": 200,
"message": "请求成功",
"data": {
"user_id": 2,
"username": "admin2",
"department_id": 2,
"departmentname": "人事部",
"roles": [
{
"id": 3,
"rolename": "人事部员工"
}
]
}
}
不出意外的话:2号用户的请求结果
{
"status": 403,
"message": "Forbidden resource",
"error": "Forbidden",
"path": "/user/1",
"timestamp": "2021-05-21T14:44:04.954Z"
}
前端的权限访问则是通过权限表url和type来处理
定时任务
nest如何开启定时任务?
定时任务场景
每天定时更新,定时发送邮件
没有controller,因为定时任务是自动完成的
yarn add @nestjs/schedule
// src/tasks/task.module.ts
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
@Module({
providers: [TasksService],
})
export class TasksModule {}
在这里编写你的定时任务
// src/tasks/task.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Cron, Interval, Timeout } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron('45 * * * * *') 每隔45秒执行一次
handleCron() {
this.logger.debug('Called when the second is 45');
}
@Interval(10000) 每隔10秒执行一次
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
@Timeout(5000) 5秒只执行一次
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
}
自定义定时时间
* * * * * * 分别对应的意思:
第1个星:秒
第2个星:分钟
第3个星:小时
第4个星:一个月中的第几天
第5个星:月
第6个星:一个星期中的第几天
如:
45 * * * * *:每隔45秒执行一次
挂载-使用
// app.module.ts
import { TasksModule } from './tasks/task.module';
import { ScheduleModule } from '@nestjs/schedule';
imports: [
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
ScheduleModule.forRoot(),
TasksModule,
],
接入Swagger接口文档
- 优点:不用写接口文档,在线生成,自动生成,可操作数据库,完美配合
dto
- 缺点:多一些代码,显得有点乱,习惯就好
yarn add @nestjs/swagger swagger-ui-express -D
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
// 创建实例
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 创建接口文档服务
const options = new DocumentBuilder()
.addBearerAuth() // token认证,输入token才可以访问文档
.setTitle('接口文档')
.setDescription('接口文档介绍') // 文档介绍
.addServer('http://localhost:9000', '开发环境')
.addServer('https://test.com/release', '正式环境')
.setVersion('1.0.0') // 文档版本
.setContact('poetry', '', 'test@qq.com')
.build();
// 为了创建完整的文档(具有定义的HTTP路由),我们使用类的createDocument()方法SwaggerModule。此方法带有两个参数,分别是应用程序实例和基本Swagger选项。
const document = SwaggerModule.createDocument(app, options, {
extraModels: [], // 这里导入模型
});
// 启动swagger
SwaggerModule.setup('api-docs', app, document); // 访问路径 http://localhost:9000/api-docs
// 启动端口
const PORT = process.env.PORT || 9000;
await app.listen(PORT, () =>
Logger.log(`服务已经启动 http://localhost:${PORT}`),
);
}
bootstrap();
swagger装饰器
@ApiTags('user') // 设置模块接口的分类,不设置默认分配到default
@ApiOperation({ summary: '标题', description: '详细描述'}) // 单个接口描述
// 传参
@ApiQuery({ name: 'limit', required: true}) // query参数
@ApiQuery({ name: 'role', enum: UserRole }) // query参数
@ApiParam({ name: 'id' }) // parma参数
@ApiBody({ type: UserCreateDTO, description: '输入用户名和密码' }) // 请求体
// 响应
@ApiResponse({
status: 200,
description: '成功返回200,失败返回400',
type: UserCreateDTO,
})
// 验证
@ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })
name: string;
在controller
引入@nestjs/swagger
, 并配置@ApiBody()
和 @ApiParam()
不写也是可以的
// user.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Query,
Param,
Delete,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import {
ApiOperation,
ApiTags,
ApiQuery,
ApiBody,
ApiResponse,
} from '@nestjs/swagger';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
@ApiTags('user') // 设置分类
export class UserController {
constructor(private readonly userService: UserService) { }
@Post()
@ApiOperation({ summary: '创建用户', description: '创建用户' }) // 该接口
@HttpCode(HttpStatus.OK)
async create(@Body() user: CreateUserDto) {
return await this.userService.create(user);
}
@Get()
@ApiOperation({ summary: '查找全部用户', description: '创建用户' })
@ApiQuery({ name: 'limit', required: true }) 请求参数
@ApiQuery({ name: 'offset', required: true }) 请求参数
async findAll(@Query() query) {
console.log(query);
const [data, count] = await this.userService.findAll(query);
return { count, data };
}
@Get(':id')
@ApiOperation({ summary: '根据ID查找用户' })
async findOne(@Param('id', new ParseIntPipe()) id: number) {
return await this.userService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: '更新用户' })
@ApiBody({ type: UpdateUserDto, description: '参数可选' }) 请求体
@ApiResponse({ 响应示例
status: 200,
description: '成功返回200,失败返回400',
type: UpdateUserDto,
})
async update(
@Param('id', new ParseIntPipe()) id: number,
@Body() user: UpdateUserDto,
) {
return await this.userService.update(id, user);
}
@Delete(':id')
@ApiOperation({ summary: '删除用户' })
async remove(@Param('id', new ParseIntPipe()) id: number) {
return await this.userService.remove(id);
}
}
编写dto,引入@nestjs/swagger
创建
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'kitty', description: '用户名' }) 添加这里即可
@IsNotEmpty({ message: '用户名不能为空' })
username: string;
@ApiProperty({ example: '12345678', description: '密码' })
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, {
message: '密码长度不能小于6位',
})
@MaxLength(20, {
message: '密码长度不能超过20位',
})
password: string;
}
更新
import {
IsEnum,
MinLength,
MaxLength,
IsOptional,
ValidateIf,
IsEmail,
IsMobilePhone,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UpdateUserDto {
@ApiProperty({ description: '用户名', example: 'kitty', required: false }) 不是必选的
@IsOptional()
username: string;
@ApiProperty({ description: '密码', example: '12345678', required: false })
@IsOptional()
@MinLength(6, {
message: '密码长度不能小于6位',
})
@MaxLength(20, {
message: '密码长度不能超过20位',
})
password: string;
@ApiProperty({
description: '邮箱',
example: 'llovenest@163.com',
required: false,
})
@IsOptional()
@IsEmail({}, { message: '邮箱格式错误' })
@ValidateIf((o) => o.username === 'admin')
email: string;
@ApiProperty({
description: '手机号码',
example: '13866668888',
required: false,
})
@IsOptional()
@IsMobilePhone('zh-CN', {}, { message: '手机号码格式错误' })
mobile: string;
@ApiProperty({
description: '性别',
example: 'female',
required: false,
enum: ['male', 'female'],
})
@IsOptional()
@IsEnum(['male', 'female'], {
message: 'gender只能传入字符串male或female',
})
gender: string;
@ApiProperty({
description: '状态',
example: 1,
required: false,
enum: [0, 1],
})
@IsOptional()
@IsEnum(
{ 禁用: 0, 可用: 1 },
{
message: 'status只能传入数字0或1',
},
)
@Type(() => Number)
status: number;
}
打开:localhost:3000/api-docs,开始测试接口