【翻译】Add JWT REST API Authentication to Your Node.js/TypeScript Backend
发布于 3 年前 作者 gocpplua 2750 次浏览 来自 分享

原文:Add JWT REST API Authentication to Your Node.js whth TypeScript Backend with TypeORM and SQLite3 Database

在本教程中,我们将学习如何使用 Node.js (Nest.js) 和 TypeScript 为 Angular 9/Ionic 5 聊天应用程序创建用于 JWT 身份验证的 REST API 服务器。

您将了解什么是 ORM 以及如何将 TypeORM 与 TypeScript 结合使用来访问和使用数据库。 您将看到什么是 JWT 以及如何使用它们在 TypeScript 和 Node 中使用 Nest.js 和 TypeORM 为数据库实现 REST API 后端的身份验证。

这些是所有教程部分:

这是使用 Node.js、TypeScript、Ionic 5 和 Angular 9 构建聊天移动应用程序系列教程的第二部分。您将学习:

  • 如何设置 TypeORM 和创建数据库,TypeORM 是 TypeScript 中最成熟的 ORM。
  • 如何在 Node 和 TypeScript 中使用 SQLite 数据库。 TypeORM 支持所有主要数据库,如 MySQL、PostgreSQL、MSSQL、Oracle 和 MongoDB,但为简单起见,我们将使用 SQLite3。
  • 如何创建用于使用用户 SQLite 数据库的 TypeORM 实体。
  • 如何创建用于处理用户数据库的 Nest.js 服务。
  • 如何在 Node 和 TypeScript 中启用 CORS。

注意:Chatkit 是 Pusher 提供的托管聊天服务,现已停用。 您可以将自己的托管聊天服务器与基于 Firebase 的 https://chatsdk.co/ 等开源解决方案结合使用,也可以使用 PubNub Chat(Chatkit 的替代付费服务)。

What’s a TypeScript ORM

根据维基百科:

计算机科学中的对象关系映射(ORM、O/RM 和 O/R 映射工具)是一种使用面向对象的编程语言在不兼容的类型系统之间转换数据的编程技术。 这实际上创建了一个可以在编程语言中使用的“虚拟对象数据库”。

ORM 并非特定于 TypeScript,而是软件和 Web 开发中的一般概念。 在本教程中,我们将使用 TypeORM,这是用于 TypeScript 的最成熟的 ORM。

TypeORM 是 TypeScript 和 JavaScript(ES7、ES6、ES5)的 ORM。 支持 MySQL、PostgreSQL、MariaDB、SQLite、MS SQL Server、Oracle、SAP Hana、WebSQL 数据库。 适用于 NodeJS、浏览器、Ionic、Cordova 和 Electron 平台。

为什么使用 SQLite

SQLite 是一个 C 语言库,它实现了一个小型、快速、自包含、高可靠性、功能齐全的 SQL 数据库引擎。 SQLite 是世界上使用最广泛的数据库引擎。 SQLite 内置于所有手机和大多数计算机中,并捆绑在人们每天使用的无数其他应用程序中。

SQLite 文件格式稳定、跨平台且向后兼容,开发人员承诺至少在 2050 年之前保持这种格式。 SQLite 数据库文件通常用作在系统之间传输丰富内容的容器,并作为长期 数据的归档格式。 有超过 1 万亿 (1e12) 个 SQLite 数据库在使用中。

SQLite 源代码位于公共领域,每个人都可以免费用于任何目的。

为简单起见,我们将使用 SQLite 数据库,但 TypeORM 支持所有主要数据库,如 MySQL、PostgreSQL、MSSQL、Oracle 和 MongoDB。

由于 ORM 抽象了对底层数据库系统的任何直接操作,因此您以后可以切换到使用成熟的系统(如 MySQL)进行生产,而无需更改代码中的任何内容。 但是现在,让我们保持简单并使用 SQLite。

设置 TypeORM 并创建 SQLite3 数据库

为了存储和注册用户,我们需要一个数据库。

Nest.js 支持 TypeORM,它被认为是 TypeScript 中可用的最成熟的对象关系映射器 (ORM)。 它可以从 @nestjs/typeorm 包中获得。

让我们从安装所需的依赖项开始:

npm install --save [@nestjs](/user/nestjs)/typeorm typeorm sqlite3

完成依赖项的安装后,您需要将 TypeOrmModule 导入到根 ApplicationModule 模块中。 打开 src/app.module.ts 文件并添加以下更改:

// server/src/app.module.ts
import { Module } from '[@nestjs](/user/nestjs)/common';
import { TypeOrmModule } from '[@nestjs](/user/nestjs)/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
   TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'my.db',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
   }),
],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

我们导入 TypeOrmModule 并调用 forRoot() 方法,该方法采用与 TypeORM 的标准 createConnection() 方法相同的配置对象。

在配置对象中,我们指定:

  • 用于类型的 sqlite 字符串,因此我们可以使用 SQLite 作为数据库,
  • 数据库文件的 my.db 字符串(SQLite 使用文件来存储数据库),
  • 实体数组,它引用以 .entity.ts 或 .entity.js 扩展名结尾的所有文件。 这些文件由开发人员创建并包含 ORM 实体。
  • synchronize选项采用 true 或 false,并允许您在每次运行应用程序时自动将数据库表与实体同步。 在开发中,您可以将其设置为 true,但在生产中并不可取。

注意:现在,您可以在任何想要访问它们的地方注入 Connection 和 EntityManager 服务。

为用户创建一个 TypeORM 实体

接下来,让我们创建一个与数据库中的用户相对应的 User 实体。 创建一个 src/models/user.entity.ts 文件并添加以下类:

// server/src/models/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  email: string;

  @Column()
  password: string;
}

您需要导入 User 实体并使用 forFeature 方法将其添加到模块的导入数组中:

// server/src/app.module.ts
import { User } from './models/user.entity';
...

@Module({
imports: [
...
TypeOrmModule.forFeature([User]),

创建用于处理用户数据库的 Nest.js 服务

接下来,让我们创建一个 UserService,它封装了我们需要对 User 模型执行的所有数据库操作。

返回终端并运行以下命令以生成服务:

$ nest g s user

此命令将创建包含实际服务代码的 src/user/user.service.ts 文件和包含服务单元测试的 src/user/user.service.spec.ts 文件。 并且还通过在 providers 数组中包含 UserService 来更新 src/app.module.ts 文件。

接下来,让我们在 src/user/user.service.ts 文件中添加 create 和 findByEmail TypeScript 方法,它们将分别用于持久化用户并通过其在数据库中的电子邮件查找用户:

// server/src/user/user.service.ts
import { Injectable } from '[@nestjs](/user/nestjs)/common';
import { User } from '../models/user.entity';
import { Repository } from 'typeorm';
import { InjectRepository } from '[@nestjs](/user/nestjs)/typeorm';

@Injectable()
export class UserService {
    constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
    ) { }

    async  findByEmail(email: string): Promise<User> {
        return await this.userRepository.findOne({
            where: {
                email: email,
            }
        });
    }

    async  create(user: User): Promise<User> {
        return await this.userRepository.save(user);
    }
}

首先我们导入 User、Repository 和 InjectRepository,接下来,通过服务的构造函数注入 User 存储库,最后我们定义我们的 TypeScript 方法。

findByEmail 方法简单地调用注入存储库的 findOne 方法,以通过数据库中传递的电子邮件搜索用户。

create 方法调用注入存储库的 save 方法将用户保存到数据库中。

使用 TypeScript 和 Node 添加 JWT 身份验证

身份验证对于大多数 Web 应用程序很重要。 您可以按照不同的方式和方法来实现用户身份验证。 在本教程中,我们将使用 JSON Web 令牌 (JWT) 实现身份验证。

什么是 JWT

根据维基百科:

JSON Web Token (JWT) 是一种 Internet 标准,用于创建具有可选签名和/或可选加密的数据,其有效负载包含 JSON,可以声明一些声明。 令牌使用私有密钥或公共/私有密钥进行签名。 例如,服务器可以生成具有“以管理员身份登录”声明的令牌,并将其提供给客户端。 然后客户端可以使用该令牌来证明它以管理员身份登录。

使用 Nest.js JWT 实用程序模块实现 JWT

首先,您需要使用以下命令为 Nest.js 安装 JWT 实用程序模块:

$ npm install --save [@nestjs](/user/nestjs)/jwt

接下来,打开 /src/app.module.ts 文件并将模块包含在导入数组中:

// server/src/app.module.ts
import { JwtModule } from  '[@nestjs](/user/nestjs)/jwt';
// [...]

JwtModule.register({
    secretOrPrivateKey:  'secret123'
})

我们还提供了一个私钥,用于对 JWT 负载进行签名。

要与我们的聊天服务器交互,您还需要有效的 JWT 令牌,这些令牌将由客户端使用令牌提供程序获取,并将随客户端向聊天服务器发出的每个请求发送。

Chatkit 提供了一个测试令牌提供程序,可用于快速开始测试聊天功能,但它应该仅用于测试。 对于生产,您需要创建自己的令牌提供程序,这可以通过两种方式完成:

  • 通过使用提供的服务器 SDK。
  • 没有服务器 SDK 的帮助,使用 JWT 库或您自己的自定义 JWT 实现。

在本教程中,我们将使用 Node.js SDK for Chatkit 在我们的 Node (Nest.js) 项目中添加令牌提供程序,因此返回到您的终端并从项目的根目录运行以下命令来安装它:

$ npm install @pusher/chatkit-server --save

接下来,让我们创建 AuthService 类,该类将封装在我们的应用程序中实现 JWT 身份验证的代码。

使用 Nest.js CLI 运行以下命令来生成服务:

$ nest g s auth

此命令将添加包含服务的 /src/auth/auth.service.ts 文件和包含服务测试的 /src/auth/auth.service.spec.ts 文件,并将更新包含的主应用程序模块 在 /src/app.module.ts 文件中包含生成的服务。

如果在此阶段打开主模块文件,可以看到 JwtauthService 已导入并包含在 providers 数组中:

// server/src/app.module.ts
import { Module } from '[@nestjs](/user/nestjs)/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthService } from './auth/auth.service';
// [...]

@Module({
  imports: [/* [...] */],
  controllers: [AppController],
  providers: [AppService, UserService,AuthService],
})
export class AppModule {}

现在,创建服务后,您需要导入 Chatkit 服务器 SDK、 JwtService 、 UserService 、 User 实体和 AuthenticationResponse。 打开 src/auth/auth.service.ts 文件并添加以下导入:

// server/src/auth/auth.service.ts
import Chatkit from '@pusher/chatkit-server';
import { JwtService } from  '[@nestjs](/user/nestjs)/jwt';
import { UserService } from  '../user/user.service';
import { User } from  '../models/user.entity';
import  Chatkit, { AuthenticationResponse } from  '@pusher/chatkit-server';

接下来,您需要添加以下代码:

// server/src/auth/auth.service.ts
@Injectable()
export class AuthService {
  chatkit: Chatkit;
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService
  ) {
    this.chatkit = new Chatkit({
      instanceLocator: YOUR_INSTANCE_LOCATOR,
      key: YOUR_SECRET_KEY
    })    
  }

我们向保存 Chatkit 实例的服务添加一个成员变量。 接下来,我们通过构造函数注入 UserService 和 JwtService,并在其中创建 Chatkit 实例。

将 YOUR_INSTANCE_LOCATOR 和 YOUR_SECRET_KEY 替换为仪表板中的凭据。 当用户连接到 Chatkit 时,将向 /token 端点(将在本教程后面创建)发送请求以对用户进行身份验证。 如果请求有效,您的服务器必须使用 Chatkit.authenticate 方法发送包含令牌的响应。

现在,您需要定义和实现以下方法:

  • getToken:用于创建和返回有效的 JWT 令牌。 此方法将简单地使用 Chatkit 实例的身份验证方法来生成有效令牌。
  • validateUser:用于验证用户。 该方法将使用 UserService 的 findByEmail 方法来检查用户是否存在于数据库中。
  • createUser:用于在本地数据库中创建用户,然后在 Chatkit 实例中创建用户。

让我们从带有 User 类型参数的 createUser 方法开始:

// server/src/auth/auth.service.ts
private async createUser(userData: User): Promise<User>{
    return this.userService.create(userData).then(user =>{
      const userId = `${user.name}${user.id}`;
      const roomId = "YOUR_ROOM_ID";
      const avatarURL = "https://image.flaticon.com/icons/png/128/149/149071.png";

      return this.chatkit.createUser({id: userId, 
         name: user.name,
         avatarURL: avatarURL
      }).then(()=>{

        return this.chatkit.addUsersToRoom({ roomId: roomId,
          userIds: [userId]}).then(()=>{
            return user;
        });

      })

    });
}

将 YOUR_ROOM_ID 替换为仪表板中的房间 ID。

该方法调用 UserService 的 create 方法将用户持久化到数据库中,然后当 Promise 成功解析到数据库中具有唯一标识符的用户对象时,我们通过调用 id 和 name 在 Chatkit 实例中创建相应的用户 实例的 createUser 方法,最后我们通过调用 addUsersToRoom 方法将用户添加到房间。

Chatkit 实例的 createUser 方法需要唯一的用户标识符和用户名。 我们通过将名称与用户的数据库 id 连接来构造用户 id。 这样我们就可以确保 Chatkit 用户 ID 是唯一的。 我们还提供了一个用户头像:https://image.flaticon.com/icons/png/128/149/149071.png, 用于进行测试。

注意:在生产应用程序中,您需要为您的用户提供一种上传他们的头像的方法,然后将他们与 Chatkit 用户相关联。 您还需要在使用 bcrypt 之类的工具将密码存储在数据库中之前对密码进行哈希处理。

现在让我们定义 getToken 方法。 它需要一个用户 ID 并返回一个 AuthenticationResponse:

// server/src/auth/auth.service.ts
public getToken(userId:  string): AuthenticationResponse {
    return this.chatkit.authenticate({ userId: userId });
}  

getToken 方法只是 Chatkit 实例的 authentication 方法的一个包装器,它返回一个有效的 JWT 令牌,客户端可以使用它来访问 Chatkit API。 authentication 方法采用我们在 Chatkit 实例中创建用户时指定的 userId(单词 name 和用户的数据库标识符的串联)。

我们需要定义的另一个方法是 validateUser 方法,它采用 User 类型的参数:

// server/src/auth/auth.service.ts
private async validateUser(userData: User): Promise<User> {
    return await this.userService.findByEmail(userData.email);
}

此方法调用 UserService 的 findByEmail 方法,该方法检查具有电子邮件的用户是否存在于数据库中。 如果存在,则返回用户对象,否则返回空对象。

定义这些方法后,我们将使用它们在同一服务中定义两个公共方法,它们是:

  • register:注册用户,
  • login:用户登录。

这是两个方法的实现:

// server/src/auth/auth.service.ts
public async login(user: User): Promise<any | {status: number}>{
    return this.validateUser(user).then((userInfo)=>{
      if(!userInfo){
        return { status: 404 };
      }
      let userId = `${userInfo.name}${userInfo.id}`;
      const accessToken = this.jwtService.sign(userId);
      return {
         expires_in: 3600,
         access_token: accessToken,
         user_id: userId,
         status: 200
      };

    });
}

public async register(user: User): Promise<any>{
    return this.createUser(user)
}

在 login 方法中,我们首先使用 validateUser 方法来确保用户存在于数据库中,然后我们调用 JwtService 的 sign 方法从用户 id 和 name 负载创建访问令牌。 最后,我们返回一个包含 expires_in、access_token、user_id 和 status 属性的对象。

在 register 方法中,我们只需调用之前定义的 createUser 方法在数据库中创建用户,然后在远程 Chatkit 实例中创建用户。

创建 REST API 端点

在实现 login 和 register 方法之后,是时候在我们的应用程序中创建相应的端点来处理用户身份验证了。 我们还需要创建一个 /token 端点,Chatkit 客户端 SDK 将使用该端点从我们的服务器请求 JWT 令牌。

打开现有的 src/app.controller.ts 文件并相应地更新它:

// server/src/app.controller.ts
import { Post, Body,Request, Controller} from '[@nestjs](/user/nestjs)/common';
import { AuthService } from './auth/auth.service';
import { User } from './models/user.entity';

@Controller()
export class AppController {
  constructor(private readonly authService: AuthService) {}

  [@Post](/user/Post)('token')
  async token([@Request](/user/Request)() req): Promise<any> {
    return this.authService.getToken(req.query.user_id).body;
  }

  [@Post](/user/Post)('login')
  async login([@Body](/user/Body)() userData: User): Promise<any> {
    return this.authService.login(userData);
  }  

  [@Post](/user/Post)('register')
  async register([@Body](/user/Body)() userData: User): Promise<any> {
    return this.authService.register(userData);
  }    
}

我们首先导入 Post、Request 和 Body 装饰器,以及 AuthService 和 User 实体。 接下来,我们通过控制器的构造函数将 AuthService 作为 authService 实例注入。

最后,我们指示 Node Nest.js 创建三个接受 POST 请求的 /token、/login 和 /register 路由,方法是用 @Post 装饰器装饰它们的方法(该路由作为参数传递)。

对于 login 和 register 方法,我们使用 @Body() 装饰器来指示 Node Nest.js 将接收到的请求的主体作为 userData 注入到端点处理程序中。

对于 token 方法,我们需要完整的请求,因此我们使用 @Request 装饰器。

注意:我们也可以使用 nest g controller auth 创建一个控制器来处理身份验证,但由于我们的 Node (Nest.js) 应用程序只有一项任务是处理 JWT 身份验证,我们可以简单地使用现有的应用程序控制器。

测试我们的 Auth REST API 端点

创建身份验证端点后,在下一个教程中创建前端移动应用程序之前,让我们使用 cURL 来测试它们。

首先,从项目的根目录运行以下命令以启动 Node (Nest.js) 开发服务器:

npm start

接下来,确保您的系统上安装了 cURL 并从终端运行以下命令:

curl -X POST -H 'content-type: application/json'  -d  '{ "email": "ahmed@gmail.com", "name": "ahmed", "password": "pass001" }' localhost:3000/register

这将在您的 SQLite 数据库中创建一个用户和一个 Chatkit 用户,您可以从 Chatkit 仪表板的控制台/实例检查器选项卡中看到该用户。 端点返回创建的 Chatkit 用户,包括 id、name、created_at 和 updated_at 字段。

您还可以使用以下方法测试 /login 端点:

curl -X POST -H 'content-type: application/json'  -d  '{ "email": "ahmed@gmail.com", "password": "pass001"}' localhost:3000/login

这应该返回一个带有访问令牌和用户 ID 的响应对象。

在 Node 和 TypeScript 中启用 CORS

由于我们将使用 Ionic 来创建将与此服务器交互的移动应用程序,并且我们将在浏览器上进行大部分 Ionic 开发,因此我们需要设置 CORS(跨源资源共享)。 否则,浏览器将因同源策略而阻止对服务器的请求。

通过打开 src/main.ts 文件并调用 app.enableCors 方法,您可以轻松地在 Node Nest.js 中启用 CORS:

// server/src/main.ts
import { NestFactory } from '[@nestjs](/user/nestjs)/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

结论

在本教程中,我们已经了解了如何使用 Node.js (Nest.js) 和 TypeScript 为 Angular 9/Ionic 5 聊天移动应用程序创建用于 JWT 身份验证的 REST API 服务器。

在下一个教程中,我们将继续使用 Angular 9 和 Ionic 5 开发我们的移动应用程序,使用此服务器进行身份验证,并使用 Chatkit 来实现聊天功能。

您可以从这个 GitHub 存储库中找到第一部分的源代码。

回到顶部