登录注册
1. 登录的流程
使用JWT实现:
用户登录:
用户在前端输入用户名和密码,并将其发送到后端进行验证。
后端验证用户的凭据,并生成一个包含用户信息的 JWT。
JWT生成:
后端使用密钥对用户信息进行签名,生成一个 JWT。
JWT 包含了用户的一些信息以及用于验证的签名信息,通常会包含用户身份、权限等信息。
JWT返回:
- 后端将生成的 JWT 发送回前端应用程序。
前端存储:
- 前端应用程序接收到 JWT 后,将其存储在本地,通常是在浏览器的本地存储(如 Local Storage 或 Session Storage)中。
后续请求:
用户在前端应用程序执行其他操作时,前端会将 JWT 添加到每个请求的 Authorization 头中。
后端服务器在收到请求后,会验证 JWT 的有效性,从而验证用户的身份和权限
2. 注册的流程
使用JWT实现:
前端注册页面:
- 前端提供用户注册表单,用户输入注册所需的信息,如用户名、密码、邮箱等。
前端验证:
- 前端应用程序对用户输入的信息进行验证,确保格式正确并满足要求。
发送注册请求:
- 用户填写完注册信息后,前端应用程序将注册信息发送到后端进行处理。
后端处理注册请求:
- 后端服务器接收到注册请求后,验证用户提供的信息的有效性,如检查用户名是否已存在。
用户信息存储和JWT生成:
如果提供的信息有效,后端服务器将用户信息存储到数据库中,并生成一个包含用户信息的 JWT。
JWT 包含了用户的一些信息以及用于验证的签名信息,通常会包含用户身份、权限等信息。
JWT返回(可选):
- 后端向前端通知用户注册成功,包含生成的 JWT 的响应(可选的)
3. Nest实现注册接口
1. 创建项目
nest new project-name
2. 连接数据库
使用Prisma连接MySQL数据库(细节见文档:Prisma)
1. 安装Prisma
安装 Prisma Cli:
npm install prisma --save-dev
使用 npx 在本地调用CLI:
npx prisma
创建初始的 Prisma 设置:
npx prisma init
2. 设置数据库连接
将schema.prisma
文件中内容更改为:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
将.env
文件中内容更改为:
DATABASE_URL="mysql://用户名:密码@域名:端口号/数据库名称"
例如:
# .env
DATABASE_URL="mysql://root:123456@localhost:3306/nest-blog"
3. 创建数据库表
我们创建一个User模型并添加到schema.prisma
文件中:
model user {
id Int @id @default(autoincrement()) @db.UnsignedInt
name String @unique
password String
}
例:
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model user {
id Int @id @default(autoincrement()) @db.UnsignedInt
name String @unique
password String
}
创建Prisma模型后,可以生成SQL迁移文件并在数据库运行它们。在终端中运行以下命令:
npx prisma migrate dev
4. 创建 PrismaClient 模块和服务
nest g mo prisma
nest g s prisma
让 PrismaService 继承自 PrismaClient:
// prisma.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
super();
}
}
将 PrismaModule 设置成全局,并导出 PrismaService:
// prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
以后我们可以在任何NestJS的服务、控制器或其他组件中注入PrismaService
,然后使用它来执行数据库操作
3. 创建认证(auth)模块
创建模块:
nest generate module auth
创建控制器:
nest generate controller auth
创建服务:
nest generate service auth
4. 创建数据传输对象(DTO)
使用DTO(数据传输对象)的主要原因之一是帮助规范和验证数据的结构和内容,以及在不同层之间传递数据。DTO可以在Nest.js应用程序中用于定义请求和响应的数据结构,并通过验证来确保数据的完整性和有效性
在auth文件夹下创建dto文件夹,在文件夹中创建register.dto.ts
文件:
// register.dto.ts
export class RegisterDto {
name: string;
password: string;
password_confirm: string;
}
5. 为DTO添加验证装饰器
管道有两个典型的应用场景:
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
我们可以使用管道中的类验证器(类验证器)来对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
我们使用class-validator
库来实现
安装依赖:
npm i --save class-validator class-transformer
使用装饰器来验证字段:
// register.dto.ts
import { IsNotEmpty } from 'class-validator';
export class RegisterDto {
@IsNotEmpty({ message: '用户名不能为空' })
name: string;
@IsNotEmpty({ message: '密码不能为空' })
password: string;
@IsNotEmpty({ message: '确认密码不能为空' })
password_confirm: string;
}
在控制器中创建接口:
// auth.controller.ts
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
@Controller('auth')
export class AuthController {
@Post('register')
async register(@Body(new ValidationPipe()) registerDto: RegisterDto) {
return registerDto;
}
}
此时我们请求/auth/register
,发送数据即可得到如下内容:
{
"name": "admin",
"password": "123456",
"password_confirm": "123456"
}
6. 为DTO添加自定义验证规则
要为 DTO(数据传输对象)添加自定义验证规则,可以使用 class-validator
库提供的自定义验证装饰器(自定义验证装饰器)
我们创建一个rules
文件夹来存放自定义验证规则,创建一个is-not-exists.rule.ts
文件,写入验证规则,验证用户名是否已经存在:
// is-not-exists.rule.ts
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
import { PrismaService } from '../prisma/prisma.service';
export function IsNotExistsRule(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'IsNotExistsRule',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
async validate(value: any, args: ValidationArguments) {
const prisma = new PrismaService();
const res = await prisma[property].findFirst({
where: {
[args.property]: value,
},
});
return !Boolean(res);
},
},
});
};
}
创建一个is-confirm.rule.ts
文件,写入验证规则,验证确认密码与密码是否一致:
// is-confirm.rule.ts
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsConfirmRule(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'IsConfirmRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
async validate(value: any, args: ValidationArguments) {
return Boolean(value == args.object[`${args.property}_confirm`]);
},
},
});
};
}
在DTO中使用:
// register.dto.ts
import { IsNotEmpty } from 'class-validator';
import { IsConfirmRule } from 'src/rules/is-confirm.rule';
import { IsNotExistsRule } from 'src/rules/is-not-exists.rule';
export class RegisterDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsNotExistsRule('user', { message: '该用户名已经注册' })
name: string;
@IsNotEmpty({ message: '密码不能为空' })
@IsConfirmRule({ message: '两次输入的密码不一致' })
password: string;
@IsNotEmpty({ message: '确认密码不能为空' })
password_confirm: string;
}
7. 完成注册服务
在auth.service.ts
中实现注册的逻辑,需要安装密码加密的工具包:
npm install argon2
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as argon2 from 'argon2';
@Injectable()
export class AuthService {
// 依赖注入
constructor(
private prisma: PrismaService,
) {}
// 注册服务
async register(dto: RegisterDto) {
// 存入数据库
const user = await this.prisma.user.create({
data: {
name: dto.name,
password: await argon2.hash(dto.password), // 对密码进行加密
},
});
return {
message: '注册成功',
user: {
id: user.id,
username: user.name,
},
};
}
}
在控制器中使用该服务:
// auth.controller.ts
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private auth: AuthService) {}
@Post('register')
async register(@Body(new ValidationPipe()) registerDto: RegisterDto) {
return this.auth.register(registerDto);
}
}
此时我们请求/auth/register
,注册成功后会返回:
{
"message": "注册成功",
"user": {
"id": 1,
"username": "admin"
}
}
4. Nest实现登录接口
1. 创建数据传输对象(DTO)
在auth文件夹下创建dto文件夹,在文件夹中创建login.dto.ts
文件:
// login.dto.ts
export class LoginDto {
name: string;
password: string;
}
2. 为DTO添加验证装饰器
使用装饰器来验证字段:
// login.dto.ts
import { IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsNotEmpty({ message: '用户名不能为空' })
name: string;
@IsNotEmpty({ message: '密码不能为空' })
password: string;
}
在控制器中创建接口:
// auth.controller.ts
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private auth: AuthService) {}
// 注册
@Post('register')
async register(@Body(new ValidationPipe()) registerDto: RegisterDto) {
return this.auth.register(registerDto);
}
// 登录
@Post('login')
async login(@Body(new ValidationPipe()) LoginDto: LoginDto) {
return LoginDto;
}
}
此时我们请求/auth/login
,发送数据即可得到如下内容:
{
"name": "admin",
"password": "123456",
}
3. 为DTO添加自定义验证规则
在rules文件夹中创建一个is-exists.rule.ts
文件,写入验证规则,验证用户名是否已经存在:
// is-exists.rule.ts
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
import { PrismaService } from '../prisma/prisma.service';
export function IsExistsRule(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'IsExistsRule',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
async validate(value: any, args: ValidationArguments) {
const prisma = new PrismaService();
const res = await prisma[property].findFirst({
where: {
[args.property]: value,
},
});
return Boolean(res);
},
},
});
};
}
在DTO中使用:
// login.dto.ts
import { IsNotEmpty } from 'class-validator';
import { IsExistsRule } from 'src/rules/is-exists.rule';
export class LoginDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsExistsRule('user', { message: '用户名不存在' })
name: string;
@IsNotEmpty({ message: '密码不能为空' })
password: string;
}
4. 实现登录的逻辑
在auth.service.ts
中实现检索用户并验证密码:
// auth.service.ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as argon2 from 'argon2';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
// 依赖注入
constructor(private prisma: PrismaService) {}
// 注册服务
async register(dto: RegisterDto) {
// 存入数据库
const user = await this.prisma.user.create({
data: {
name: dto.name,
password: await argon2.hash(dto.password), // 对密码进行加密
},
});
return {
message: '注册成功',
user: {
id: user.id,
username: user.name,
},
};
}
// 登录服务
async login(dto: LoginDto) {
const user = await this.prisma.user.findUnique({
where: {
name: dto.name,
},
});
if (!(await argon2.verify(user.password, dto.password))) {
throw new BadRequestException('密码错误');
}
return {
message: '登录成功',
};
}
}
在控制器中调用:
// auth.controller.ts
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private auth: AuthService) {}
@Post('register')
async register(@Body(new ValidationPipe()) registerDto: RegisterDto) {
return this.auth.register(registerDto);
}
@Post('login')
async login(@Body(new ValidationPipe()) LoginDto: LoginDto) {
return this.auth.login(LoginDto);
}
}
此时我们请求/auth/login
,登录成功后会返回:
{
"message": "登录成功"
}
5. 使用JWT
我们使用Passport库来实现
详见:Passport
安装Passport相关包:
npm install --save @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-local @types/passport-jwt
注入 JwtService
,并更新生成JWT令牌的方法:
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService){}
// 更新生成JWT令牌的方法
}
例:
// auth.service.ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as argon2 from 'argon2';
import { LoginDto } from './dto/login.dto';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
// 依赖注入
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
// 注册服务
async register(dto: RegisterDto) {
// 存入数据库
const user = await this.prisma.user.create({
data: {
name: dto.name,
password: await argon2.hash(dto.password), // 对密码进行加密
},
});
return {
message: '注册成功',
user: {
id: user.id,
username: user.name,
},
};
}
// 登录服务
async login(dto: LoginDto) {
const user = await this.prisma.user.findUnique({
where: {
name: dto.name,
},
});
if (!(await argon2.verify(user.password, dto.password))) {
throw new BadRequestException('密码错误');
}
// payload 包含了用户的名称和标识(id)
const payload = { username: user.name, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
在auth文件夹下创建constants.ts
文件中设置token密钥:
// constants.ts
export const jwtConstants = {
secret: 'zhf',
};
我们将使用它在 JWT 签名和验证步骤之间共享密钥
在auth.module.ts
文件中配置:
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '100d' },
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
此时我们请求/auth/login
,登录成功后会返回token:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJzdWIiOjEsImlhdCI6MTcwMTg0NjM2NCwiZXhwIjoxNzEwNDg2MzY0fQ.l7GsYbO4d8iq_CcrjMjJapzWRs0DftR5ruesaU5QHZg"
}