登录注册

Mr.ZhaoAbout 9 min

1. 登录的流程

使用JWT实现:

  1. 用户登录

    • 用户在前端输入用户名和密码,并将其发送到后端进行验证。

    • 后端验证用户的凭据,并生成一个包含用户信息的 JWT。

  2. JWT生成

    • 后端使用密钥对用户信息进行签名,生成一个 JWT。

    • JWT 包含了用户的一些信息以及用于验证的签名信息,通常会包含用户身份、权限等信息。

  3. JWT返回

    • 后端将生成的 JWT 发送回前端应用程序。
  4. 前端存储

    • 前端应用程序接收到 JWT 后,将其存储在本地,通常是在浏览器的本地存储(如 Local Storage 或 Session Storage)中。
  5. 后续请求

    • 用户在前端应用程序执行其他操作时,前端会将 JWT 添加到每个请求的 Authorization 头中。

    • 后端服务器在收到请求后,会验证 JWT 的有效性,从而验证用户的身份和权限

2. 注册的流程

使用JWT实现:

  1. 前端注册页面

    • 前端提供用户注册表单,用户输入注册所需的信息,如用户名、密码、邮箱等。
  2. 前端验证

    • 前端应用程序对用户输入的信息进行验证,确保格式正确并满足要求。
  3. 发送注册请求

    • 用户填写完注册信息后,前端应用程序将注册信息发送到后端进行处理。
  4. 后端处理注册请求

    • 后端服务器接收到注册请求后,验证用户提供的信息的有效性,如检查用户名是否已存在。
  5. 用户信息存储和JWT生成

    • 如果提供的信息有效,后端服务器将用户信息存储到数据库中,并生成一个包含用户信息的 JWT。

    • JWT 包含了用户的一些信息以及用于验证的签名信息,通常会包含用户身份、权限等信息。

  6. JWT返回(可选)

    • 后端向前端通知用户注册成功,包含生成的 JWT 的响应(可选的)

3. Nest实现注册接口

1. 创建项目

nest new project-name

2. 连接数据库

使用Prisma连接MySQL数据库(细节见文档:Prismaopen in new window

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添加验证装饰器

管道有两个典型的应用场景:

  • 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常

我们可以使用管道中的类验证器(类验证器open in new window)来对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常

我们使用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 库提供的自定义验证装饰器(自定义验证装饰器open in new window

我们创建一个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库来实现

详见:Passportopen in new window

安装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"
}