安装nestjs
npm i -g @nestjs/cli
创建项目
nest new project-name
可以选择npm、yarn、npmp,这里选择yarn
再依赖安装完毕之后,可以使用如下命令启动 NestJS 应用,然后浏览器即可访问 http://localhost:3000/ :出现如下界面即代表项目已经正常启动了。
数据库
安装依赖
选用mysql数据库,安装数据库依赖。
yarn add mysql typeorm @nestjs/typeorm
配置数据库
配置数据库,在app.module.ts
// 根模块用于处理其他类的引用与共享。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { MenuModule } from './menu/menu.module';
import { RoleModule } from './role/role.module';
import { UserRoleModule } from './user_role/user_role.module';
import { join } from 'path';
@Module({
imports: [
// 加载连接数据库
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'vite_node',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
logging: false,
}),
// 子模块
UserModule,
AuthModule,
MenuModule,
RoleModule,
UserRoleModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
数据表
以下是基础数据表
menu.sqlrole.sqlrole_menu.sqluser.sqluser_role.sql
使用swagger
安装依赖
yarn add @nestjs/swagger swagger-ui-express
配置swagger
在nest-cli.json
和 main.ts
文件中进行配置,然后启动项目访问http:localhost:3001/docs 即可。
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"generateOptions": {
"spec": false
},
"compilerOptions": {
"plugins": ["@nestjs/swagger"]
}
}
// 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// Swagger 模块
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';
//配置 swagger
const setupSwagger = (app) => {
const config = new DocumentBuilder()
.addBearerAuth()
.setTitle('NEST API')
.setDescription('nest-test的 API 文档')
.setVersion('1.0')
.build();
// 创建
const document = SwaggerModule.createDocument(app, config);
// 启动
SwaggerModule.setup('doc', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
});
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 传入app 访问http:localhost:3001/doc
setupSwagger(app);
await app.listen(3001);
}
bootstrap();
根据数据库数据表生成entity-typeorm-model-generator
"db": "npx typeorm-model-generator -h localhost -d vite_node -p 3306 -u root -x 123456 -e mysql -o ./src/entities --noConfig true --ce pascal --cp camel -a"
- npx typeorm-model-generator :如果全局安装了,npx就不用
- -h localhost :ip
- -d vite_node :数据库名字
- -p 3306 :数据库端口号
- -u root :用户名
- -x 123456 :密码
- -e mysql :什么数据库
- -o ./src/entities :生成实体的地址
- –noConfig true :表示不生成
ormconfig.json
和tsconfig.json
文件 - –ce pascal :类名转换成首字母大写的驼峰命名
- –cp camel :将数据库中的字段也转换成驼峰命名,比如user_role转换成userRole
- -a:表示实体会继承
BaseEntity
的类
类如User,关于一些注解的说明。
-
@PrimaryGeneratedColumn
:主键id -
@ApiProperty
:关于这个字段的说明 @Column
:
- nullable:是否可以为空。
- name:此字段表示在数据库中的列名,类如驼峰命名转换:login_name ——> loginaName。
- select:查询时是否返回此字段
-
@Exclude
:使用此注解时,可以实现对返回的数据过滤掉此列,需要在controller层对某一个方法上加上注解@UseInterceptors(ClassSerializerInterceptor)
对应@Exclude()列,表示此列隐藏。 -
@OneToMany
:一对多的关系
import {
BaseEntity,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from "typeorm";
import { ApiProperty } from '@nestjs/swagger';
import { UserRole } from "./UserRole.entity";
import { Exclude } from "class-transformer";
import { IsEnum } from "class-validator";
@Entity("user", { schema: "vite_node" })
export class User extends BaseEntity {
@ApiProperty({ description: '自增 id' })
@PrimaryGeneratedColumn({ type: "int", name: "id" })
id: number;
// 表示select: false 查询user时不会返回这个字段,隐藏此列,也可以使用
// @Column({ select: false })
@ApiProperty({ description: '密码' })
@Exclude()
@Column("varchar", { name: "password", length: 255 })
password: string;
@ApiProperty({ description: '性别', example: 1, required: false, enum: [0, 1], })
@IsEnum(
{ 女: 0, 男: 1 },
{
message: ' sex 只能传入数字0或1',
},
)
@Column("int", { default: 1, name: "sex",})
sex?: number;
@ApiProperty({ description: '邮箱', required: false })
@Column("varchar", { name: "email", nullable: true, length: 255 })
email: string | null;
@ApiProperty({ description: '地址' })
@Column("varchar", { name: "address", nullable: true, length: 255 })
address: string | null;
@ApiProperty({ description: '登录名' })
@Column("varchar", { name: "login_name", length: 255 })
loginName: string;
@ApiProperty({ description: '用户名' })
@Column("varchar", { name: "user_name", length: 255 })
userName: string;
@OneToMany(() => UserRole, (userRole) => userRole.user)
userRoles: UserRole[];
}
创建资源 CRUD 生成器
这里先创建user表的
nest g resource [name]
// 简写 res
nest g res user
]
登录-权限-认证-守卫 jwt
安装依赖
- @nestjs/passport:模块将该框架包装在一个 Nest 风格的包中,使其易于集成到 Nest 应用程序中。
- @nestjs/jwt :身份认证
- @types/passport-jwt:编写 TypeScript 代码时提供了帮助
- passport-jwt:策略包(还有passport-local)
- passport:是node目前最流行的身份认证库,与使用@nestjs/passport的nestjs结合使用非常简单。
策略有本地策略和jwt策略,这里使用的是jwt
yarn add @nestjs/jwt passport-jwt @types/passport-jwt @nestjs/passport passport
创建auth相关模块(module、service、controller)
nest g mo auth
nest g s auth
nest g c auth
在AuthService
中需要进行登录验证的时候需要操作UserService
,所以在UserModule
的exports中将其暴露出去,以供其他模块使用。然后在AuthModule
的providers中添加UserService
。
import { AuthModule } from '../auth/auth.module';
import { UserRole } from '../entities/UserRole.entity';
import {
Module,
NestModule,
MiddlewareConsumer,
RequestMethod,
forwardRef,
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../entities/User.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { LoggerMiddleware } from '../middle-ware/logger.middleware';
@Module({
imports: [TypeOrmModule.forFeature([User, UserRole])],
controllers: [UserController],
providers: [UserService],
// 将 UserService 暴露出去
exports: [UserService]
})
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'user', method: RequestMethod.GET });
}
}
import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";
@Module({
imports: [
JwtModule.register({
secret: JWT_CONSTANT.secret,
signOptions: { expiresIn: '4h' }
}),
// 使用 UserModule 的内容
UserModule
],
providers: [
AuthService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard, //挂载全局接口
},
],
controllers: [AuthController],
})
export class AuthModule {}
然后需要创建jwt.strategy.ts
写jwt的验证策略。
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { JWT_CONSTANT } from "./jwt.constant";
import { User } from "src/entities/User.entity";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: JWT_CONSTANT.secret,
});
}
async validate(payload: User) {
return { loginName: payload.loginName, id: payload.id };
}
}
然后在写一个守卫来做一个验证。
import {
ExecutionContext,
Injectable,
UnauthorizedException,
SetMetadata,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { RedisInstance } from "src/utils/redis";
// @SkipAuth 跳过JWT验证
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector,
private jwtService: JwtService) {
super();
}
async canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
console.log('---re--',request);
const authorization = request["headers"].authorization || void 0;
let tokenNotTimeOut = true;
if (authorization) {
const token = authorization.split(" ")[1]; // authorization: Bearer xxx
try {
let payload: any = this.jwtService.decode(token);
const key = `${payload.id}-${payload.loginName}`;
const redis_token = await RedisInstance.getRedis(
"jwt-auth.guard.canActivate",
0,
key
);
if (!redis_token || redis_token !== token) {
throw new UnauthorizedException("请重新登录");
}
} catch (err) {
tokenNotTimeOut = false;
throw new UnauthorizedException("请重新登录");
}
}
return tokenNotTimeOut && (super.canActivate(context) as boolean);
// return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
console.log(err, user, info);
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
// swager装饰器:需要验证token的controller中增加装饰器@ApiBearerAuth,增加后swager请求header会携带Authorization参数。
// 生成跳过检测装饰器 @SkipAuth()
export const IS_PUBLIC_KEY = "isPublic";
export const SkipAuth = () => SetMetadata(IS_PUBLIC_KEY, true);
然后我们将其引入到auth模块中,再写登录接口
import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";
@Module({
imports: [
JwtModule.register({
secret: JWT_CONSTANT.secret,
signOptions: { expiresIn: '4h' }
}),
// 使用 UserModule 的内容
UserModule
],
providers: [
AuthService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard, //挂载全局接口
},
],
controllers: [AuthController],
})
export class AuthModule {}
import { Body, Controller, Post } from "@nestjs/common";
import { ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger";
import { LoginUserDto } from "./dto/login-user.dto";
import { AuthService } from "./auth.service";
import { SkipAuth } from "./jwt-auth.guard";// 跳过登录
@Controller("auth")
@ApiTags("用户验证")
export class AuthController {
constructor(private authService: AuthService) {}
@SkipAuth()
@Post("login.do")
@ApiBody({type:LoginUserDto})
@ApiOperation({
summary: "用户登录",
})
async loginUser(@Body() loginUserDto: LoginUserDto) {
return await this.authService.login(loginUserDto);
}
}
这里需要注意的是,登录时密码加密使用的是crypto,token存储使用redis来进行对比,下面部分会单独说明redis的引入。
在AuthService
中使用UserService
中方法findOneByLoginName
、getUserRole
。( private readonly userService: UserService)
import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { LoginUserDto } from "./dto/login-user.dto";
import { Encrypt } from 'src/utils/crypto'
import { UserService } from "../user/user.service";
import { RedisInstance } from "src/utils/redis";
const logger = new Logger("auth.service");
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private jwtService: JwtService
) { }
/**
* @description: 用户登录
* @param {User} loginUserDto
* @return {*}
*/
public async login(loginUserDto: LoginUserDto) {
let data = {}
try {
const loginName: string = loginUserDto.loginName;
const password: string = loginUserDto.password;
const userInfo = await this.userService.findOneByLoginName(loginName);
if (userInfo.length === 0) {
data = { flag: false, msg: "用户不存在" };
return;
}
// 加密
// const pass = Encrypt(password)
// if (pass === userInfo[0].password) {
if (password === userInfo[0].password) {
const token = await this.createToken(userInfo[0]);
//存储token到redis
const redis = await RedisInstance.initRedis("auth.login", 0);
const key = `${userInfo[0].id}-${loginUserDto.loginName}`;
await RedisInstance.setRedis("auth.login", 0, key, `${token}`);
data = {
flag: true,
msg: "登录成功",
user: {
userInfo: userInfo[0],
},
token,
};
} else {
data = { flag: false, msg: "用户密码错误" };
}
} catch (error) {
logger.log(error);
data = { flag: false, msg: "登录失败" };
} finally {
return data;
}
}
/**
* @description:创建token
* @param loginUserDto
* @returns
*/
private async createToken(loginUserDto: LoginUserDto) {
const payload = {
loginName: loginUserDto.loginName,
password: loginUserDto.password,
id: loginUserDto.id
};
return this.jwtService.sign(payload);
}
}
UserService
中实现以上所调用的方法。
import { Injectable } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from '../entities/User.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
// 测试加密
import { Encrypt } from 'src/utils/crypto'
// import { User } from 'src/entities/User.entity';
@Injectable()
export class UserService {
// 使用InjectRepository装饰器并引入Repository这样就可以使用typeorm的操作了
// 引入 InjectRepository typeOrm 依赖注入 接受一个实体
// 引入类型 Repository 接受实体泛型
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) { }
/**
* @description: 创建用户
* @param createUserDto
* @returns
*/
async create(createUserDto: CreateUserDto) {
const { userName, loginName, password } = createUserDto;
const userInfo = await this.findOneByLoginName(loginName);
if (userInfo.length > 0) {
return { flag: false, msg: "登录名已存在" }
}
const user = new User();
user.userName = userName;
user.loginName = loginName;
user.password = Encrypt(password);
user.sex = 1;
return this.userRepository.save(user);
}
/**
* @description: 查询所有用户
* @param query
* @returns
*/
async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }) {
// async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }): Promise<User[]> {
const list = await this.userRepository.find({
where: [
{ loginName: Like(`%${query.keyWord}%`) },
{ userName: Like(`%${query.keyWord}%`) }
],
order: {
id: "DESC"
},
skip: (query.pageCurrent - 1) * query.pageSize,
take: query.pageSize
})
const total = list.length
return { list, total }
// return await this.userRepository.query('select * from user');
}
/**
* @description: 查询是否存在此用户,登录名不重复
* @param loginName
* @returns
*/
async findOneByLoginName(loginName: string): Promise<User[]> {
return await this.userRepository.find({
where: {
loginName: loginName
}
});
}
/**
* @description: 根据id查找用户信息
* @param id
* @returns
*/
async findOne(id: number) {
return await this.userRepository.find({
where: {
id: id
}
});
}
/**
* @description 更新用户
* @param id
* @param updateUserDto
* @returns
*/
update(id: number, updateUserDto: UpdateUserDto) {
return this.userRepository.update(id, updateUserDto);
}
/**
* @description 删除用户
* @param id
* @returns
*/
remove(id: number) {
return this.userRepository.delete(id);
}
}
// 跨域
app.enableCors()
使用redis,增加token过期和单点登录
安装依赖
yarn add ioredis
下载redis
下载redishttps://github.com/MSOpenTech/redis/releases
设置密码
到redis目录下,命令行运行:redis-server.exe redis.windows.conf
另起窗口redis-cli.exe -h 127.0.0.1 -p 6379 查看密码 config get requirepass
设置密码:config set requirepass password
再登录有密码的redis: redis-cli -p 6379 -a password
redis-cli.exe -h 127.0.0.1 -p 6379 -a password
项目中引用resid
配置redis
然后创建redis.ts
文件
import { Logger } from "@nestjs/common";
import Redis from "ioredis";
const logger = new Logger("auth.service");
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
const redisOption = {
host: "127.0.0.1",
port: 6379,
password: "password",
};
export class RedisInstance {
static async initRedis(method: string, db = 0) {
const isExist = redisIndex.some((x) => x === db);
if (!isExist) {
Logger.debug(
`[Redis ${db}]来自 ${method} 方法调用 `
);
redisList[db] = new Redis({ ...redisOption, db });
redisIndex.push(db);
} else {
Logger.debug(`[Redis ${db}]来自 ${method} 方法调用`);
}
return redisList[db];
}
static async setRedis(
method: string,
db = 0,
key: string,
val: string,
timeout = 60 * 60
) {
if (typeof val == "object") {
val = JSON.stringify(val);
}
const redis = await RedisInstance.initRedis(method, db);
redis.set(`${key}`, val);
redis.expire(`${key}`, timeout);
}
static async getRedis(method: string, db = 0, key: string) {
return new Promise(async (resolve, reject) => {
const redis = await RedisInstance.initRedis(method, db);
redis.get(`${key}`, (err, val) => {
if (err) {
reject(err);
return;
}
resolve(val);
});
});
}
}