Fork me on GitHub

nestjs入门学习总结(五):实现用户登录注册功能

实现用户注册

  1. 我们先使用命令创建两个模块,分别是用户模块和授权模块
nest g resource auth

nest g resource user
  1. 编写用户实体
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';

@Entity('tb_user')
export class UserEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({
    type: 'varchar',
    length: 50,
    unique: true,
    comment: '用户名',
  })
  username: string;

  @Column({
    type: 'varchar',
    length: 50,
    nullable: true,
    comment: '昵称',
  })
  nickname: string;

  @Column({
    type: 'datetime',
    nullable: true,
    comment: '生日',
  })
  birthday: Date;

  @Exclude()
  @Column({
    type: 'varchar',
    length: 100,
    comment: '密码',
  })
  password: string;

  @Column({
    type: 'varchar',
    length: 50,
    nullable: true,
    comment: '邮箱',
  })
  email: string;

  @Column({
    name: 'created_at',
    type: 'datetime',
    // default: () => 'NOW()',
    default: () => 'CURRENT_TIMESTAMP',
    comment: '创建时间',
  })
  createdAt: Date;

  @Column({
    name: 'updated_at',
    type: 'datetime',
    // default: () => 'NOW()',
    default: () => 'CURRENT_TIMESTAMP',
    comment: '更新时间',
  })
  updatedAt: Date;
}
  1. 编写authService和userService
// auth.service.ts

/**
  * 用户注册
  * @param createUserDto
  * @returns
  */
async register(createUserDto: CreateUserDto) {
  const { username, password } = createUserDto;
  const existUser = await this.userService.findByUsername(username);
  if (existUser) {
    throw new BadRequestException('注册用户已存在');
  }

  const user = {
    ...createUserDto,
    password: encryptPwd(password), // 保存加密后的密码
  };

  return await this.userService.create(user);
}


// user.service.ts

/**
   * 根据用户名查询用户信息
   * @param username
   * @returns
   */
  async findByUsername(username: string) {
    return await this.userRepository.findOne({
      where: { username },
    });
  }

/**
   * 创建新用户
   */
  async create(user: CreateUserDto) {
    const { username } = user;
    await this.userRepository.save(user);
    return await this.userRepository.findOne({
      where: { username },
    });
  }
  1. 密码加密处理

用户注册密码,需要对密码加密后再保存,这里我们使用bcryptjs这个库来对密码进行加密处理

bcryptjs 是 nodejs 中比较出色的一款处理加盐加密的包。
所谓加盐,就是在加密的基础上再加点“佐料”。这个“佐料”是系统随机生成的一个随机值,并且以随机的方式混在加密之后的密码中。
由于“佐料”是系统随机生成的,相同的原始密码在加入“佐料”之后,都会生成不同的字符串。
这样就大大的增加了破解的难度。

/**
 * 加密处理 - 同步方法
 * bcryptjs.hashSync(data, salt)
 *    - data  要加密的数据
 *    - slat  用于哈希密码的盐。如果指定为数字,则将使用指定的轮数生成盐并将其使用。推荐 10
 */
const hashPassword = bcryptjs.hashSync(password, 10)

/**
 * 校验 - 使用同步方法
 * bcryptjs.compareSync(data, encrypted)
 *    - data        要比较的数据, 使用登录时传递过来的密码
 *    - encrypted   要比较的数据, 使用从数据库中查询出来的加密过的密码
 */

const isOk = bcryptjs.compareSync(password, encryptPassword)

安装使用

yarn add bcryptjs

// 引入 bcryptjs
const bcryptjs = require('bcryptjs')

/**
 * 密码加密
 * @param password 注册密码
 * @returns 加密后的密码
 */
export const encryptPwd = (password) => {
  return bcryptjs.hashSync(password, 10);
};
  1. 编写authController方法路由
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
  return this.authService.register(createUserDto);
}
  1. 发起api请求

发起post注册请求 http://localhost:3000/auth/register

请求参数

{
    "username": "kerrywu",
    "password": "123456"
}

返回结果

{
    "code": 0,
    "msg": "success",
    "data": {
        "id": "d20e01eb-3f71-4b63-87f2-78296fadf40b",
        "username": "kerrywu17",
        "nickname": null,
        "birthday": null,
        "email": null,
        "createdAt": "2023-07-07T18:37:49.000Z",
        "updatedAt": "2023-07-07T18:37:49.000Z"
    }
}

请求结果过滤,不返回用户密码

  1. 对需要过滤的字段添加@Exclude()装饰器
import { Exclude } from 'class-transformer';

@Exclude()
@Column({
  type: 'varchar',
  length: 100,
  comment: '密码',
})
password: string;
  1. 在对应请求的地方标记使用ClassSerializerInterceptor,此时该请求返回的数据中就不会包含password字段
import {
  Controller,
  Post,
  Body,
  UseInterceptors,
  ClassSerializerInterceptor,
} from '@nestjs/common';

// 绑定拦截器ClassSerializerInterceptor,返回结果过滤掉password
@UseInterceptors(ClassSerializerInterceptor)
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
  return this.authService.register(createUserDto);
}

3. 以上方式对执行save方法返回不生效,可以findOne查询后再返回

/**
   * 创建新用户
   */
async create(user: CreateUserDto) {
  const { username } = user;
  await this.userRepository.save(user);
  return await this.userRepository.findOne({
    where: { username },
  });
}

实现用户登录

  1. 编写authService,实现登录逻辑,返回token
// auth.service.ts

/**
   * 用户登录
   * @param createUserDto
   * @returns
   */
  async login(loginUserDto: LoginUserDto) {
    const existUser = await this.validateUser(loginUserDto);
    const token = this.createToken(existUser);
    return {
      userId: existUser.id,
      token,
    };
  }

/**
   * 校验登录用户
   * @param user
   * @returns
   */
  async validateUser(user) {
    const { username, password } = user;
    const existUser = await this.userService.findByUsername(username);
    if (!existUser) {
      throw new BadRequestException('用户不存在');
    }
    const { password: encryptPwd } = existUser;
    const isOk = comparePwd(password, encryptPwd);
    if (!isOk) {
      throw new BadRequestException('登录密码错误');
    }
    return existUser;
  }

  /**
   * 创建token
   * @param user
   * @returns
   */
  createToken(user) {
    const payload = {
      id: user.id,
      username: user.username,
    };
    return this.jwtService.sign(payload);
  }
  1. 编写authController,实现登录请求方法
@Post('login')
  async login(@Body() loginUserDto: LoginUserDto) {
    return this.authService.login(loginUserDto);
  }
  1. 发起post登录请求

http://localhost:3000/auth/login

请求参数

{
    "username": "kerrywu9",
    "password": "123456"
}

返回结果

{
    "code": 0,
    "msg": "success",
    "data": {
        "userId": "4824c8bd-b540-476a-98bf-3fa71d2382d6",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQ4MjRjOGJkLWI1NDAtNDc2YS05OGJmLTNmYTcxZDIzODJkNiIsInVzZXJuYW1lIjoia2Vycnl3dTkiLCJpYXQiOjE2ODg3NTQwNTksImV4cCI6MTY4ODc2ODQ1OX0.eQsBJBLQZnzmFupQQIfEqCQM8Vt2JKrRhoM7yhPk36w"
    }
}

token发放、接口鉴权

我们使用jwt来生成token,生成是一串字符串

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQ4MjRjOGJkLWI1NDAtNDc2YS05OGJmLTNmYTcxZDIzODJkNiIsInVzZXJuYW1lIjoia2Vycnl3dTkiLCJpYXQiOjE2ODg3NTQwNTksImV4cCI6MTY4ODc2ODQ1OX0.eQsBJBLQZnzmFupQQIfEqCQM8Vt2JKrRhoM7yhPk36w

JWT 由三个部分组成,它们通过点号(.)分隔:

  • 头部(Header):描述令牌的元数据和签名算法。
  • 载荷(Payload):包含声明信息,例如用户身份、权限等。
  • 签名(Signature):用于验证令牌的完整性和真实性。

JWT 验证流程

  1. 接收到 JWT 后,首先将其拆分为头部、载荷和签名三个部分。

  2. 验证签名:使用事先共享的密钥和签名算法对头部和载荷进行签名验证,确保令牌未被篡改。

  3. 检查有效期:检查载荷中的声明,例如过期时间(exp)和生效时间(nbf),确保令牌在有效时间范围内。

  4. 可选的其他验证:根据需要,可能还会验证其他声明,如发行者(iss)、受众(aud)等。

一旦 JWT 通过验证,可以信任其内容,并根据其中的声明执行相应的操作。常见的用途包括用户身份验证、授权访问资源和传递用户信息等。需要注意的是,JWT 的安全性依赖于密钥的保护和正确的实现。同时,由于 JWT 本身包含了用户信息,因此在传输过程中需要采取适当的安全措施,如使用 HTTPS 来保护通信。

jwt生成token

  1. 安装nest提供的jwt包
yarn add @nestjs/jwt
  1. 注册JwtModule

在模块中要使用jwt,我们需要先注册JwtModule

// auth.module.ts

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

// 注册JwtModule
// const jwtModule = JwtModule.register({
//   secret: jwtConstants.secret,
//   signOptions: { expiresIn: '60s' }, // 单位:120ms/60s/4h/7d
// });

// 异步方式注册JwtModule
const jwtModule = JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => {
    return {
      secret: configService.get('JWT_SECRET'),
      signOptions: { expiresIn: '4h' }, // 设置token过期时间,单位:120ms/60s/4h/7d
    };
  },
});

@Module({
  imports: [jwtModule],
  providers: [AuthService],
  exports: [jwtModule],
  controllers: [AuthController],
})
export class AuthModule {}
  1. 使用jwtService.sign方法生成token
// auth.service.ts

import { JwtService } from '@nestjs/jwt';

constructor(
    // 在authService类构造函数中注入JwtService
    private readonly jwtService: JwtService,
  ) {}

/**
  * 创建token
  * @param user
  * @returns
  */
createToken(user) {
  const payload = {
    id: user.id,
    username: user.username,
  };
  return this.jwtService.sign(payload);
}

接口鉴权

用户登录成功后返回给前端jwt生成的token,前端请求需要鉴权的接口时候携带该token,后端对token进行鉴权验证

Passport是最流行的 node.js 身份验证库,实现token验证,nest提供了对Passport的包装@nestjs/passport模块

  1. 安装库及对应的ts类型
yarn add @nestjs/passport passport-jwt

yarn add -D @types/passport-jwt @types/passport
  1. 实现jwt策略

对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递

// jwt.strategy.ts

import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
// import { jwtConstants } from './constants';

/**
 * 实现jwt策略
 * 对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly configService: ConfigService) {
    super({
      // 提供从请求中提取 JWT 的方法。
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 选择默认的 false 设置,它将确保 JWT 没有过期的责任委托给 Passport 模块。这意味着,如果我们的路由提供了一个过期的 JWT ,请求将被拒绝,并发送 401 Unauthorized 的响应。
      ignoreExpiration: false,
      // 密钥,不要暴露出去
      secretOrKey: configService.get('JWT_SECRET'),
      //   secretOrKey: jwtConstants.secret,
    } as StrategyOptions);
  }

  // payload {
  //     id: '4d8b498e-b743-46e4-bdf8-91c25212441b',
  //     username: 'kerrywu',
  //     iat: 1688724765,
  //     exp: 1688739165
  //   }
  async validate(payload: any) {
    return { userId: payload.id, username: payload.username };
  }
}
  1. 导入PassportModule模块、注入JwtStrategy
// auth.module.ts

import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [jwtModule, PassportModule, UserModule],
  providers: [AuthService, JwtStrategy],
  exports: [jwtModule],
  controllers: [AuthController],
})
  1. 最后在需要的controller方法中绑定jwt授权守卫
// link.controller.ts

import {
  Post,
  Body,
  Req,
  UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';


@UseGuards(AuthGuard('jwt'))
  @Post()
  create(@Body() createLinkDto: CreateLinkDto, @Req() req) {
    const { userId } = req.user;
    return this.linkService.create(userId, createLinkDto);
  }

验证通过后,从@Req() req里面我们就可以拿到jwt的用户信息

// 从@Req() req里面可以拿到jwt的用户信息
  // user: {
  //   userId: '4d8b498e-b743-46e4-bdf8-91c25212441b',
  //   username: 'kerrywu'
  // }

报错解决

error TS2559: Type 'string' has no properties in common with type 'FindOneOptions'.

42 return await this.userRepository.findOne(id);

说是版本问题
https://www.ithao.net/question/192418

您使用的是最新版本的 typeorm 吗?然后将其降级为 typeorm@0.2 因为 @nestjs/typeorm@8.0 可能还不支持最新的。您可以在此处阅读 typeorm@0.3 的更改:https://github.com/typeorm/typeorm/releases/tag/0.3.0

项目源码

代码已经上传到github中,欢迎大家star,持续更新,如有任何问题可以联系我v:sky201208(注明来意)

https://github.com/fozero/cloud-collect-nestjs

参考阅读

关于我&前端&node进阶交流学习群

大家好,我是阿健Kerry,一个有趣且乐于分享的人,前小鹏汽车、货拉拉高级前端工程师,长期专注前端开发,如果你对前端&Node.js 学习进阶感兴趣的话(后续有计划也可以),可以关注我,加我微信【sky201208】,拉你进交流群一起交流、学习共同进步,群内氛围特别好,定期会组织技术分享~

posted @ 2023-07-13 23:38  fozero  阅读(999)  评论(0编辑  收藏  举报