前言

上一篇介绍了如何使用 Sequelize 连接 MySQL,接下来,在原来代码的基础上进行扩展,实现用户的注册和登录功能。

这里简单提一下 JWT:

具体原理可以参考《JSON Web Token 入门教程 - 阮一峰》

所以 JWT 实现【登录】的大致流程是:

  1. 客户端用户进行登录请求;
  2. 服务端拿到请求,根据参数查询用户表;
  3. 若匹配到用户,将用户信息进行签证,并颁发 Token;
  4. 客户端拿到 Token 后,存储至某一地方,在之后的请求中都带上 Token ;
  5. 服务端接收到带 Token 的请求后,直接根据签证进行校验,无需再查询用户信息;

下面,就开始我们的实战:

GitHub 项目地址,欢迎各位大佬 Star。

在 目录下,新建文件夹 utils,里面将存放各种工具函数,然后新建 cryptogram.ts 文件:

上面写了两个方法,一个是制作一个随机盐(salt),另一个是根据盐来加密密码。

这两个函数将贯穿注册和登录的功能。

二、用户注册

在写注册逻辑之前,我们需要先修改一下上一篇写过的代码,即 user.service.ts 中的 findeOne() 方法:

    现在,findOne() 的功能更符合它的方法名了,查到了,就返回用户信息,查不到,就返回 undefined

    接下来,我们开始编写注册功能:

    1. // src/logical/user/user.service.ts
      import { Injectable } from '@nestjs/common';
      import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
      import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

      import { makeSalt, encryptPassword } from '../../utils/cryptogram'; // 引入加密函数

      @Injectable()
      export class UserService {
      /**
      * 查询是否有该用户
      * @param username 用户名
      */
      async findOne(username: string): Promise<any | undefined> {
      ...
      }

      /**
      * 注册
      * @param requestBody 请求体
      */
      async register(requestBody: any): Promise<any> {
      const { accountName, realName, password, repassword, mobile } = requestBody;
      if (password !== repassword) {
      return {
      code: 400,
      msg: '两次密码输入不一致',
      };
      }
      const user = await this.findOne(accountName);
      if (user) {
      return {
      code: 400,
      msg: '用户已存在',
      };
      }
      const salt = makeSalt(); // 制作密码盐
      const hashPwd = encryptPassword(password, salt); // 加密密码
      const registerSQL = `
      INSERT INTO admin_user
      (account_name, real_name, passwd, passwd_salt, mobile, user_status, role, create_by)
      VALUES
      ('${accountName}', '${realName}', '${hashPwd}', '${salt}', '${mobile}', 1, 3, 0)
      `;
      try {
      await sequelize.query(registerSQL, { logging: false });
      return {
      code: 200,
      msg: 'Success',
      };
      } catch (error) {
      return {
      code: 503,
      msg: `Service error: ${error}`,
      };
      }
      }
      }

    编写好后,在 user.controller.ts 中添加路由

    1. // src/logical/user/user.controller.ts
      import { Controller, Post, Body } from '@nestjs/common';
      import { UserService } from './user.service';

      @Controller('user')
      export class UserController {
      constructor(private readonly usersService: UserService) {}

      // @Post('find-one')
      // findOne(@Body() body: any) {
      // return this.usersService.findOne(body.username);
      // }

      @Post('register')
      async register(@Body() body: any) {
      return await this.usersService.register(body);
      }
      }

    现在,我们使用 Postman 来测试一下,先故意输入不一样的密码和已存在的用户名:

    如图,密码不一致的校验触发了。

    然后,我们把密码改成一致的:

    如图,已有用户的校验触发了。

    然后,我们再输入正确的参数:

    发现已经将信息插入表中了,而且密码也是加密后的,至此,注册功能已基本完成。

    为了更直观的感受处理顺序,我在代码中加入了步骤打印

    1. 安装依赖包

    2. 创建 Auth 模块

    1. $ nest g service auth logical
      $ nest g module auth logical

    在 文件夹下新增一个 constants.ts,用于存储各种用到的常量:

    1. // src/logical/auth/constats.ts
      export const jwtConstants = {
      secret: 'shinobi7414', // 秘钥
      };

    4. 编写 JWT 策略

    auth 文件夹下新增一个 jwt.strategy.ts,用于编写 JWT 的验证策略:

    1. // src/logical/auth/jwt.strategy.ts
      import { ExtractJwt, Strategy } from 'passport-jwt';
      import { PassportStrategy } from '@nestjs/passport';
      import { Injectable } from '@nestjs/common';
      import { jwtConstants } from './constants';

      @Injectable()
      export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor() {
      super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
      });
      }

      // JWT验证 - Step 4: 被守卫调用
      async validate(payload: any) {
      console.log(`JWT验证 - Step 4: 被守卫调用`);
      return {
      userId: payload.sub,
      username: payload.username,
      realName: payload.realName,
      role: payload.role,
      };
      }
      }

    5. 编写 auth.service.ts 的验证逻辑

    此时保存文件,控制台会报错:

    可以先不管,这是因为还没有把 JwtService 和 UserService 关联到 auth.module.ts 中。

    这一步非必须,根据项目的需求来决定是否需要本地策略

    1. // src/logical/auth/local.strategy.ts
      import { Strategy } from 'passport-local';
      import { PassportStrategy } from '@nestjs/passport';
      import { Injectable, UnauthorizedException } from '@nestjs/common';
      import { AuthService } from './auth.service';

      @Injectable()
      export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(private readonly authService: AuthService) {
      super();
      }

      async validate(username: string, password: string): Promise<any> {
      const user = await this.authService.validateUser(username, password);
      if (!user) {
      throw new UnauthorizedException();
      }
      return user;
      }
      }

    6. 关联 Module

    1. // src/logical/auth/auth.module.ts
      import { Module } from '@nestjs/common';
      import { AuthService } from './auth.service';
      import { LocalStrategy } from './local.strategy';
      import { JwtStrategy } from './jwt.strategy';
      import { UserModule } from '../user/user.module';
      import { PassportModule } from '@nestjs/passport';
      import { JwtModule } from '@nestjs/jwt';
      import { jwtConstants } from './constants';

      @Module({
      imports: [
      PassportModule.register({ defaultStrategy: 'jwt' }),
      JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '8h' }, // token 过期时效
      }),
      UserModule,
      ],
      providers: [AuthService, LocalStrategy, JwtStrategy],
      exports: [AuthService],
      })
      export class AuthModule {}

    此时保存文件,若还有上文的报错,则需要去 app.module.ts,将 AuthServiceproviders 数组中移除,并在 imports 数组中添加 即可:

    1. // src/app.module.ts
      import { Module } from '@nestjs/common';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { UserModule } from './logical/user/user.module';
      // import { AuthService } from './logical/auth/auth.service';
      import { AuthModule } from './logical/auth/auth.module';

      @Module({
      imports: [UserModule, AuthModule],
      controllers: [AppController],
      providers: [AppService],
      })
      export class AppModule {}

    7. 编写 login 路由

    此时,回归到 user.controller.ts,我们将组装好的 JWT 相关文件引入,并根据验证码来判断用户状态:

    此时保存文件,同样的报错又出现了:

    这次我们先去 user.module.tscontrollers 注释掉:

    此时看控制台,没有 User 相关的路由,我们需要去 app.module.ts 将 Controller 添加回去:

    这么做是因为如果在 user.module.ts 中引入 AuthService 的话,就还要将其他的策略又引入一次,个人觉得很麻烦,就干脆直接用 app 来统一管理了。

    四、登录验证

    前面列了一大堆代码,是时候检验效果了,我们就按照原来注册的信息,进行登录请求:

    三、使用 JWT 实现注册、登录 - 图1

    图中可以看到,已经返回了一长串 token 了,而且控制台也打印了登录的步骤和用户信息。前端拿到这个 token,就可以请求其他有守卫的接口了。

    三、使用 JWT 实现注册、登录 - 图2

    既然发放了 Token,就要能验证 Token,因此就要用到 Guard(守卫)了。

    我们拿之前的注册接口测试一下,修改 user.controller.ts 的代码,引入 UseGuardsAuthGuard,并在路由上添加 @UseGuards(AuthGuard('jwt'))

    然后,我们先来试试请求头没有带 token 的情况:

    可以看到,返回 401 状态码,Unauthorized 表示未授权,也就是判断你没有登录。

    现在,我们试试带 Token 的情况,把登录拿到的 Token 复制到 Postman 的 Authorzation 里(选择 Bearer Token):

    三、使用 JWT 实现注册、登录 - 图3

    然后再请求接口:

    此时,已经可以正常访问了,再看看控制台打印的信息,步骤也正如代码中注释的那样:

    三、使用 JWT 实现注册、登录 - 图4

    至此,登录功能已基本完成。

    总结

    本篇介绍了如何使用 JWT 对用户登录进行 Token 签发,并在接受到含 Token 请求的时候,如何验证用户信息,从而实现了登录验证。

    当然,实现登录验证并不局限于 JWT,还有很多方法,有兴趣的读者可以自己查阅。

    这里也说一下 JWT 的缺点,主要是无法在使用同一账号登录的情况下,后登录的,挤掉先登录的,也就是让先前的 Token 失效,从而保证信息安全(至少我是没查到相关解决方法,如果有大神解决过该问题,还请指点),只能使用一些其他黑科技挤掉 Token(如 Redis)。

    现在,注册、登录功能都有了,接下来应该完善一个服务端应有的其他公共功能。

    下一篇将介绍拦截器、异常处理以及日志的收集。

    本篇收录于 NestJS 实战教程,更多文章敬请关注。

    `

    • 本文作者: 图雀社区
    • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!