序言
现在nodejs的框架已经很多了,我之所以选择nestjs是因为感觉这个框架和Java写起来很像,想接触一下后台开发。我基于mall这个项目的框架来学习,打算完成和这个项目一样的商城项目,在这之中我会记录我自己学习的经历,希望可以帮助大家,当然也希望可以从大家的经验中学习,如何觉得这个项目对你说有所帮助,希望动动您发财的小手帮我Git star~~~~
唠叨两句
有一些简单的东西或者自己可以很容易查到的资料,我就不在文章中说了,我主要是基于我的项目来说,因为东一榔头西一棒槌的感觉就是再查资料,会把自己搞混,所以我的目标是能分享出自己这个项目遇到的问题就好。当然我也会把一些资料放到文章最后,也可以做个参考。
项目说明
开始之前,先说明一下我用到的环境,否则有可能下载项目以后出现无法运行的情况,我的项目分为nest写的后端项目和基于vue2的前端项目,需要说明的是这个vue项目mall_admin也是从mall拿过来的,我之所以放到我的Git上了,一个是因为我改了一个地方,另外一个是觉得mall这个项目可能会继续更新代码,到时候你们下载下来可能就和我的nestjs项目对应不上了。
nestjs项目
npm6.14.7nestjs9.0.0 我觉得这个版本应该不是大版本就没事,只能说我接触的时候是9版本nodev14.8.0
vue管理系统 这个运行的时候,我需要说明一下,他这个项目用node:14.8.0运行不起来,我这里是切换成了node:12.6.0运行起来的,开发前端的应该都安装了nvm这个工具来切换版本了,就这里需要注意
开始吧
从前端调后台接口角度来讲,前端首先接口就是登录注册,然后获取用户信息,存储token,所以我的第一篇教程也是先从这方面开始。
一、 登录
登录需要做什么呢,除了用户名和密码,其实更重要的就是返回token给前端,作为访问接口的凭证,这需要nestjs判断账号和密码正确与否,然后再需要的地方加上token的校验. 客户端用户进行登录请求; 服务端拿到请求,根据参数查询用户表; 若匹配到用户,将用户信息进行签证,并颁发 Token; 客户端拿到 Token 后,存储至某一地方,在之后的请求中都带上 Token ; 服务端接收到带 Token 的请求后,直接根据签名进行校验,无需再查询用户信息;
- 首先创建相关的模块,通过这个命令可以快速创建出模块里面的module,service等,反正就是都有了
nest -g res auth .src/modules
- 创建数据库表对应的实体,这个对应的就是数据库里面的
ums_admin表,有两个地方需要说明一下
- hashPassword这里用到了BeforeInsert注解,目的是加密一下代码
- password这个字段用到了这个@Exclude()注解,目的是返回数据的时候可以过滤到password字段,密码比较重要,尽量不要返回。
import { ApiProperty } from '@nestjs/swagger';
import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Exclude } from 'class-transformer';
@Entity('ums_admin')
export class Auth {
@ApiProperty({ description: '自增 id' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: '名称' })
@Column({ length: 500 })
username: string;
@ApiProperty({ description: '密码' })
@Column({ length: 500 })
@Exclude()
password: string;
@ApiProperty({ description: '邮箱' })
@Column({ length: 20 })
email: string;
@ApiProperty({ description: '图标' })
@Column({ length: 500 })
icon: string;
@ApiProperty({ description: '昵称' })
@Column({ name: 'nick_name', length: 100 })
nickName: string;
@ApiProperty({ description: '标记' })
@Column({ length: 500 })
note: string;
@Column({
name: 'create_time',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@BeforeInsert()
createDate() {
// 更新entity前更新LastUpdatedDate
this.createTime = new Date();
}
@Column({
name: 'login_time',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
loginTime: Date;
@ApiProperty({ description: '状态' })
@Column()
status: number;
@BeforeInsert()
private async hashPassword() {
const salt = await bcrypt.genSalt();
console.log('salt', salt, this.password);
this.password = await bcrypt.hash(this.password, salt);
}
}
上面说的不返回密码字段,需要在对应的方法上面使用 @UseInterceptors(ClassSerializerInterceptor)才会生效,对应的代码auth.controller.ts这个控制器里面.
@ApiOperation({
summary: '后台注册',
})
@ApiBody({ type: RegisterDto, description: '后台注册' })
@SkipJwtAuth()
@UseInterceptors(ClassSerializerInterceptor)
@Post('register')
async register(@Request() req, @Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
其实到这里,通过authController直接返回已经就可以调用接口了,但是我们要用到jwt验证所以需要继续修改
二、jkt验证
jwt是一种token验证的策略,每种语言都有对应的实现,到我的项目就是需要考虑三点,一是生成token返回前端 二是项目本身怎么去验证token 三是token的续期
1. 生成token
首先需要安装相关的库,
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
安装完成之后,和数据库什么一样,需要引入模块,然后配置参数
@Module({
imports: [
//这里就是jwt的配置了
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
console.log('configService.get', configService.get('jwt.secret'));
return {
secret: configService.get<string>('jwt.secret'),
};
},
}),
],
controllers: [AuthController],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
],
exports: [AuthService],
})
export class AuthModule {}
然后创建策略文件,local的作用就是验证账号密码,通过以后把user信息存起来,给jkt生成token使用
仅仅有了这两个还不行,怎么才能让他们起作用呢,这就需要用到nestjs里面守卫guard,这里的守卫其实和前端路由守卫差不多一个意思,就是过滤掉不能访问角色或者权限。
下面是两个策略的文件,文件位置在src/modules/auth/strategies文件夹下
local.strategy.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-local';
import { User } from '@src/modules/user/entities/user.entity';
import { AuthService } from '../auth.service';
// 本地策略,验证用户名和密码
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
usernameField: 'username',
passwordField: 'password',
} as IStrategyOptions);
}
// 这个是自带的方法,使用策略后会执行这个方法
async validate(
username: string,
password: string,
): Promise<Omit<User, 'password'>> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new HttpException('用户名或秘密错误', HttpStatus.OK);
}
return user;
}
}
jwt.strategy.ts,,这里面的validate方法就是生成token的方法,把生成token需要的信息传过去就可以了
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '@src/modules/user/user.service';
import { ConfigService } from '@nestjs/config';
import { RedisCacheService } from '@src/modules/redis-cache/redis-cache.service';
import { Request } from 'express';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private userService: UserService,
private configService: ConfigService,
private redisCacheService: RedisCacheService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.secret'),
});
}
async validate(payload: any) {
try {
console.log('payload', payload);
const existUser = this.userService.findOne(payload.sub);
if (!existUser) {
throw new UnauthorizedException();
}
return { ...payload, id: payload.sub };
} catch (error) {
console.log('error', error);
}
}
}
下面是两个守卫的文件,文件位置在src/modules/auth/guards文件夹下
local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
jwt-auth.guard.ts
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../constants';
import { JwtService } from '@nestjs/jwt';
import { RedisCacheService } from '@src/modules/redis-cache/redis-cache.service';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private configService: ConfigService,
private redisCacheService: RedisCacheService,
private jwtService: JwtService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.secret'),
});
let tokenNotTimeOut = true;
try {
const key = `${payload.id}-${payload.username}`;
const redis_token = await this.redisCacheService.cacheGet(key);
if (redis_token !== token) {
throw new UnauthorizedException('请重新登录');
}
this.redisCacheService.cacheSet(
key,
token,
this.configService.get<number>('redis.token_expire'),
);
} catch (err) {
tokenNotTimeOut = false;
throw new UnauthorizedException('请重新登录');
}
return tokenNotTimeOut && (super.canActivate(context) as boolean);
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
handleRequest(err, user) {
// 处理 info
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
2. 验证token
通过上面的步骤,已经有了token,这时候需要启用两个守卫才可以通过接口验证token,下面是authcontroller里面的相关接口,登录接口我们用了
@UseGuards(LocalAuthGuard)和@SkipJwtAuth(),而另外一个接口什么也没有用,这是为什么,因为我们大部分接口都要验证,不能每一个接口都去单独加jwt守卫吧,而SkipJwtAuth就是不验证token的接口。
@ApiOperation({
summary: '后台登录',
})
@ApiBody({ type: LoginDto, description: '后台登录' })
@SkipJwtAuth()
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
console.log('configService', this.configService.get('jwt.secret'));
return this.authService.login(req.user);
}
@ApiOperation({
summary: '获取当前登录用户的菜单以及对应的角色list',
description: '获取当前登录用户的菜单',
})
@Get('info')
async getAdminInfo(@Request() req) {
console.log('req.user', req.user);
const user = req.user;
const menuList = await this.permissionService.getMenuList(user.id);
const roleList = await this.roleService.getRoleListById(user.id);
const result = {
username: user.username,
menus: menuList,
roles: roleList,
};
return result;
}
全局验证需要再auth.module.ts中加上
providers: [
AuthService,
RedisCacheService,
LocalStrategy,
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
],
而不需要验证的文件是 auth/constants.ts,这里是在什么地方作用的,其实是在jwt-auth.guard.ts里面
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const SkipJwtAuth = () => SetMetadata(IS_PUBLIC_KEY, true);
上面文件jwt-auth.guard.ts里面的canActivatef方法,这个方法就是处理能不能访问这个接口的关键代码true就是通过了
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
总结
最后说几点上面没提的需要注意一点
-
我们必须引入auth实体才能使用对应的表, 如auht.module.ts文件,每次都不要忘了,因为我经常忘,所以老是报依赖错误
@Module({
imports: [
TypeOrmModule.forFeature([Auth]),
UserModule,
PermissionModule,
PassportModule,
RoleModule,
RedisCacheModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
console.log('configService.get', configService.get('jwt.secret'));
return {
secret: configService.get<string>('jwt.secret'),
};
},
}),
],
controllers: [AuthController],
providers: [
AuthService,
RedisCacheService,
LocalStrategy,
JwtStrategy,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
],
exports: [AuthService],
})
export class AuthModule {}
-
上面里面的
secret: configService.get<string>('jwt.secret')这个是我弄的配置文件,再config->yaml里面。