在Vue3 + Nest 实现博客管理系统 后端篇(一)中已经完成了Nest项目的构建,以及数据库的连接,接下来要做的是用户表的设计以及登录注册功能的实现:
完善用户表
在篇幅一中已经完成了用户表的创建,只有两个字段是不符合我们后续开发的要求,目前关于用户表的设计包含:id
、用户名
、昵称
、密码
、角色
、生日
、头像
、性别
、邮箱
等等,其中在注册的时候用户名和密码是不能为空的,其他的看自己要求
在后续的表创建中都会涉及到几个字段:id
、创建时间
、更新时间
所以我们可以把他们单独抽离一个文件 base.entity.ts
在根目录的libs下面创建文件 Entities/base.entity.ts
内容如下:
ts
import { CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Base {
@PrimaryGeneratedColumn('uuid')
id: string; // 主键,自动生成
@CreateDateColumn({
type: 'timestamp',
nullable: false,
comment: '创建时间',
})
create_at: Date;
@CreateDateColumn({
type: 'timestamp',
nullable: false,
comment: '更新时间',
})
update_at: Date;
}
然后修改User模块中设置用户实体user/entities/user.entity.ts
文件
ts
import { Base } from 'libs/Entities/base.entity';
import { Column, Entity } from 'typeorm';
@Entity('user')
export class User extends Base {
@Column({
type: 'varchar',
length: 50,
unique: true,
comment: '用户名',
})
username: string;
@Column({
type: 'varchar',
length: 50,
comment: '密码',
})
password: string;
@Column({
nullable: true,
comment: '角色',
})
role: string;
@Column({
nullable: true,
comment: '昵称',
})
nickname: string;
@Column({
nullable: true,
comment: '年龄',
})
age: number;
@Column({
nullable: true,
comment: '头像',
})
avatar: string;
@Column({
nullable: true,
comment: '性别',
})
sex: string;
@Column({
nullable: true,
comment: '邮箱',
})
email: string;
}
表设计完成之后开始实现注册功能,在注册过程中首先判断注册的用户是否已经存在数据库中,如果已经存在抛出相对应的状态码以及错误信息,否则就向数据库中添加一条用户数据,同时在存入数据库的时候要对密码进行加密
首先针对账号和密码做一些规则校验:不能为空,长度限制等等,我们这里使用ValidationPipe + class-validator
安装 class-validator 和 class-transformer 的包:
bash
pnpm install class-validator
在CreateUserDto
里声明参数的约束条件:
ts
import { IsString, IsNotEmpty, Length, Matches } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@Length(6, 30)
@Matches(/^[a-zA-Z0-9#$%_-]+$/, {
message: '用户名只能是字母、数字或者 #、$、%、_、- 这些字符',
})
username: string;
@IsString()
@IsNotEmpty()
@Length(6, 30)
password: string;
}
首先在user/dto/create-user.dto.ts
文件里面添加两个字段
ts
export class CreateUserDto {
username: string;
password: string;
}
其次在user.controller.ts
文件里面添加注册register
接口,并且使用 ValidationPipe
进行约束
对于密码要进行加密处理,为了更好的复用,我们在src目录下面创建utils/md5.ts
文件,并且使用crypto包进行加密:
首先安装crypto
:
bash
pnpm install crypto
创建md5加密方法
ts
import * as crypto from 'crypto';
/**
*
* @param str 要加密解密的字符
* @returns
*/
export function md5(str: string): string {
if (!str) return '';
const hash = crypto.createHash('md5');
return hash.update(str).digest('hex');
}
然后在user.service.ts
文件里面编写注册逻辑
ts
async register(createUserDto: CreateUserDto) {
const { username, password } = createUserDto;
const existUser = await this.userRepository.findOneBy({
username,
});
if (existUser) {
throw new HttpException('用户名已存在', 200);
}
// 对密码进行加密
const newUser = new User();
newUser.username = username;
newUser.password = md5(password);
try {
return await this.userRepository.save(newUser);
} catch (e) {
throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
然后进行接口注册测试:
但是返回的信息包含密码,虽然已经加密处理,首先在 user.entity.ts
做一下修改
然后修改 user.module.ts
再次注册
注册接口就告一段落了,接下来开始实现登录功能
登录功能
首先在 user.controller.ts
添加登录接口
然后在 user.service.ts
里面添加登录的方法
ts
// 登录
async login(createUserDto: CreateUserDto) {
const { username, password } = createUserDto;
const existUser = await this.userRepository.findOne({
where: {
username,
},
select: ['username', 'password'],
});
if (!existUser) {
throw new HttpException('用户名不存在', 200);
}
if (existUser.password != md5(password)) {
throw new HttpException('密码错误,请重新登录', 200);
}
return existUser;
}
然后登录测试
这里就已经登录成功了,之后我们我们要用到JWT来根据用户名和密码生成一个token返回给前端,前端每次请求后端都会校验token是否失效
安装 @nestjs/jwt
bash
pnpm install @nestjs/jwt
修改default 环境配置
env
DB_HOST = localhost
DB_PORT = 3306
DB_USERNAME = root
DB_PASSWORD = 123456
DB_DATABASE = manageadmin
DB_SYNC = true
# 服务端口号
SERVE_PORT = 3333
# JWT密码/失效时间
JWT_SECRET = dapeng
JWT_EXPIRESIN = 7d
然后修改我们的 db.module.ts
ts
JwtModule.registerAsync({
global: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: configService.get('JWT_EXPIRESIN'),
},
}),
}),
修改 user.control.ts
的登录方法
再次登录能够看到生成的token
使用Guard限制访问
前端登录之后拿到token,在后面的所有请求会把token放到请求头里面,后端需要拿到这个token看是否过期,如果已经过期返回前端重新登录,我们可以自定义一个装饰器Guard,在每个请求都加上进行约束 执行nest g guard Jwt.auth --no-spec --flat
生成一个Jwt.auth.guard.ts文件,如果让选择是在src还是db下面,选择src即可(看自己),没有的话就不用不管
bash
nest g guard Jwt.auth --no-spec --flat
修改jwt.auth.guard.ts
ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest(); // 获取请求头
const token = this.extractTokenFromHeader(request); // 获取请求中的token字段
if (!token) {
throw new UnauthorizedException('登录 token 错误,请重新登录');
}
try {
const payload = await this.jwtService.verify(token, {
secret: this.configService.get('JWT_SECRET'),
});
request['user'] = payload;
} catch (e) {
throw new UnauthorizedException('身份过期,请重新登录');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
修改 user.controller.ts
, 在findAll 添加装饰器 UseGuards,测试Jwt鉴权
ts
@Inject(JwtService)
private jwtService: JwtService;
@Post('login')
async login(@Body() createUserDto: CreateUserDto) {
const foundUser = await this.userService.login(createUserDto);
if (foundUser) {
const payload = {
username: foundUser.username,
sub: foundUser.id,
};
return this.jwtService.sign({
user: payload,
});
}
}
@Get()
@UseGuards(JwtAuthGuard)
findAll() {
return this.userService.findAll();
}
使用账号登录获取token ,然后再查询用户接口的请求头添加 Authorization
值为:Bearer
加token
能够正常的查询到数据
白名单和全局Guard
用户登录之后所有的方法都要加上Jwt鉴权,我们需要把JwAuthGuard 设置为全局Guard
在app.module.ts
文件里面
ts
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
然后去掉刚才在findAll的Jwt鉴权UseGuards
然后再次查询用户数据也是能够正常返回的,这个时候Jwt全局鉴权已经添加完毕,但是当我们执行登录的时候不应该进行鉴权的
接下来是针对一些不需要鉴权的接口单独处理,首先创建文件utils/public.ts
@SetMetadata()
装饰器来为路由处理方法设置元数据,这里设置元数据IsPublic为true
ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
然后在不需要鉴权的地方添加修饰器@Public()
这样就可以在jwt.auth.guard.ts
里面通过Reflector 取出当前的isPublic,如果为true(使用了@Public()装饰过的接口),就不进行鉴权
ts
// 一些接口不需要进行认证(方法和白名单路由差不多)
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
接下来就开始设置白名单,对于一些请求直接放行,不需要鉴权,首先在jwt.auth.guard.ts
里面添加白名单,以及判断白名单的逻辑
ts
// 白名单放行
if (this.hasUrl(this.urlList, request.url)) {
return true;
}
private urlList: string[] = ['/user/login', '/user/register']; // 验证该次请求是否为白名单内的路由
private hasUrl(urlList: string[], url: string): boolean {
const flag = urlList.indexOf(url) >= 0;
return flag;
}
前几步我们给login路由添加了@Pulic()装饰,现在我们把他去掉,来验证白名单是否生效
token正常返回,白名单生效,好了,本章节学习就告一段落了