hapi.js_使用Hapi.js制作RESTful API

论坛 期权论坛 编程之家     
选择匿名的用户   2021-5-23 05:57   399   0

hapi.js

介绍 ( Introduction )

As the century dawns, we see new technologies and architectures which companies, startups and developers, alike, are using to power their next big application; some of these architectures have been so throughly battle-tested, that some companies are even scrapping old application just so that they can implement this modern, new (and sclable) approach.

随着世纪的到来,我们看到了公司,初创公司和开发人员都在使用新技术和体系结构来推动其下一个大型应用程序的发展。 这些架构中的一些经过了严格的测试,以至于有些公司甚至放弃了旧的应用程序,以便他们可以实施这种现代的,新的(可伸缩的)方法。

One of these architectures is the Representational State Transfer or REST as the people call it. In this architecture, the server talks in terms of resources, and then uses HTTP verbs to perform actions on those resources. A quick detour of this architecture has been taken in a following section.

其中一个架构是重新表象小号大老贸易交接或REST的人把它。 在这种体系结构中,服务器根据资源进行交谈,然后使用HTTP动词对这些资源执行操作。 下一节将快速绕过此体系结构。

With Node.js, it’s not doubt that it’s super-easy to implement a quick RESTful API and be up and running in no time; however, there are quite a few considerations before someone thinks of implementing a scalable, high-efficiency RESTful API.

使用Node.js,毫无疑问,实现快速的RESTful API并立即启动并运行非常容易。 但是,在考虑实现可扩展的高效RESTful API之前,有很多考虑事项。

This article bulids on my previous article on Hapi.js. In case you find this confusing, please refer to the Part 1 of this tutorial. Once you're through that one, you can come here and complete the entire series.

本文与我以前关于Hapi.js的文章有关。 如果您感到困惑,请参阅本教程的第1部分 。 完成该课程后,您可以来到这里并完成整个系列。

为什么要使用Hapi.js? (Why am I using Hapi.js?)

Honestly, I am a HUGE fan of the Hapi.js framework, and the Hapi.js community. Cheers to all those who have contributed to this remarkable project.

老实说,我非常喜欢Hapi.js框架和Hapi.js社区。 为所有为这个非凡项目做出贡献的人们欢呼。

Hapi.js makes things so simple, and so smooth to work with; all that without compromising on the reliability or efficiency of the framework. In case you haven’t read it yet, I have a small article here which, as mentioned earlier, covers just enough material to get your feet wet and kicking for this project.

Hapi.js使事情变得如此简单,流畅。 所有这些都不会影响框架的可靠性或效率。 如果您还没有阅读它,我在这里有一篇小文章,如前所述,该文章仅涵盖足够的材料来使您的脚湿透并踢踢这个项目。

目标 (Goals)

Now, before we start out work on this API, let’s set a couple of goals. Here the word ‘goals’ is like the success criterion of your application. Since this is a project to help you get up to speed with Hapi.js, we’ll really add the features which are unique: display or implement some of Hapi.js’ philosophy or concepts. It’ll also trim the dead wood from our experimental code-base.

现在,在开始使用此API之前,让我们设定一些目标。 这里的“目标”一词就像您申请的成功标准。 由于这是一个帮助您快速熟悉Hapi.js的项目,因此我们将真正添加独特的功能:显示或实现一些Hapi.js的理念或概念。 它还将从我们的实验代码库中修剪掉死木。

主要API功能 (Primary API Function)

This API is the Developer API for a startup which returns the birda spotted in a particular area.

此API是启动程序的开发人员API,该启动程序返回发现在特定区域的鸟形标语。

逻辑目标 (Logical Goals)

  • All users can see every public birda in the database;

    所有用户都可以看到数据库中的每个公共鸟类。
  • All users can login/out;

    所有用户均可登录/注销;
  • Registered users can create birds;

    注册用户可以创建鸟类;
  • Registered users can edit birds provided they own that bird;

    注册用户只要拥有该鸟,就可以对其进行编辑;
  • Registered users can delete the birds.

    注册用户可以删除鸟类。

I presume everything in this section is quite self-explanatory. However, if you are confused about something, feel free to drop us a comment in the comments section of this post.

我认为本节中的所有内容都是不言自明的。 但是,如果您对某些事情感到困惑,请随时在本文的评论部分给我们评论。

RESTful架构简介 ( Introduction to the RESTful Architecture )

In a RESTfully-architectured web application, you let the HTTP verbs (like GET, POST, etc.) do the work of telling your application what to do. For this tutorial, we'll have routes something like the following:

RESTfully体系结构的Web应用程序中,您让HTTP动词(例如GETPOST等)完成告诉应用程序要做什么的工作。 在本教程中,我们将提供如下路线:

  • http://api.app.com/birds (GET) - to get a list of all the public birds

    http://api.app.com/birdsGET ) -让所有的名单public
  • http://api.app.com/birds (POST) - to create a new bird

    http://api.app.com/birdsPOST ) -创建一个新的鸟
  • http://api.app.com/birds/:id (GET) - to get a specific bird

    http://api.app.com/birds/:idGET ) -得到特定的鸟

This is something quite standard of any RESTful API. There is a pretty good post on Scotch about designing APIs with RESTful architecture in mind. Take a look.

这是任何RESTful API的标准。 在Scotch上有一篇很好的文章,关于考虑RESTful体系结构设计API。 看一看。

入门 ( Getting Started )

With everything said, let's dig right into it. To test the API, I will be using this fantastic application called Paw. Alternatively, you can use ARC or Postman App.

说了一切,让我们深入研究它。 为了测试API,我将使用名为Paw的出色应用程序。 或者,您可以使用ARCPostman App

选择工具 (Tools of Choice)

To create this awesome API, we'll be using a couple of very interesting Node.js packages.

为了创建这个很棒的API,我们将使用几个非常有趣的Node.js包。

Knex.js

Knex.js

Knex is a very simple to use, yet incredibly powerful query builder for MySQL and a plethora of other RDBMS. We'll use this to directly communicate with our Authentication and Data servers running MySQL.

Knex是一个非常简单易用的查询工具,但功能强大,可用于MySQL和其他众多RDBMS。 我们将使用它与运行MySQL的身份验证和数据服务器直接通信。

Hapi.js

Hapi.js

Hapi (pronounced "happy") is a web framework for building web applications, APIs and services. It's extremely simple to get started with, and extremely powerful at the same time. The problem arises when you have to write perfomant, maintable code. You can take a look at my getting started tutorial to get started with it, and then this tutorial as a continuation.

Hapi(发音为“ happy”)是用于构建Web应用程序,API和服务的Web框架。 入门非常简单,同时功能非常强大。 当您必须编写性能主表代码时,就会出现问题。 您可以查看我的入门教程以开始使用它,然后作为后续教程继续学习。

Alright, perfect. Now we understand the nuances of this application and we can begin coding.

好的,完美。 现在我们了解了此应用程序的细微差别,可以开始编码了。

设置package.json ( Setting up package.json )

Initialize a new package.json file with npm init in your root folder and then filling the values as required. The following is my package.json:

在根文件夹中使用npm init初始化一个新的package.json文件,然后根据需要填充值。 以下是我的package.json

{
  "name": "birdbase",
  "version": "1.0.0",
}

Now, let's install mysql, jsonwebtoken, hapi-auth-jwt, and knex with

现在,使用以下命令安装mysqljsonwebtokenhapi-auth-jwtknex

npm i --save mysql jsonwebtoken hapi-auth-jwt knex

Note that this configuration is based on the configuration of my previous article. I haven't included everything and this is just a build up on that. Be sure to follow that one before this.

请注意,此配置基于我上一篇文章的配置。 我还没有包括所有内容,而这只是在此基础上建立的。 在此之前,请务必遵循该步骤。

Now, our package.json looks something like:

现在,我们的package.json看起来像:

{
  "name": "birdbase",
  "version": "1.0.0",
  "devDependencies": {
    "babel-core": "^6.20.0",
    "babel-preset-es2015": "^6.18.0"
  },
  "dependencies": {
    "hapi": "^16.0.1",
    "hapi-auth-jwt": "^4.0.0",
    "jsonwebtoken": "^7.2.1",
    "knex": "^0.12.6",
    "mysql": "^2.12.0"
  },
  "scripts": {
    "start": "node bootstrap.js"
  }
}

And the following is the directory structure:

以下是目录结构:

Perfect. Now, let's configure Knex so we can begin working with it.

完善。 现在,让我们配置Knex,以便我们开始使用它。

设置Knex ( Setting up Knex )

Knex is just brilliant. And we will see the reasons to that brilliance in just a minute. Start by installing the knex cli with sudo npm install -g knex. This tools allows us to programatically create a MySQL table structure and then execute it. Generally, we call this migrations.

Knex才华横溢。 我们将在一分钟内看到这种出色的原因。 首先使用sudo npm install -g knex安装knex cli 。 该工具允许我们以编程方式创建一个MySQL表结构,然后执行它。 通常,我们称这种migrations

For the rest of the tutorial, the following is my MySQL configuration:

对于本教程的其余部分,以下是我MySQL配置:

MySQL Host: 192.168.33.10
MySQL User: birdbase
MySQL Pass: password
MySQL DB Name: birdbase

Create a knexfile.js in the root of the directory with the following content:

在目录的根目录中创建具有以下内容的knexfile.js

module.exports = {

    development: {

        migrations: { tableName: 'knex_migrations' },
        seeds: { tableName: './seeds' },

        client: 'mysql',
        connection: {

            host: '192.168.33.10',

            user: 'birdbase',
            password: 'password',

            database: 'birdbase',
            charset: 'utf8',

        }

    }

};

And create a new folder called seeds in the root directory. The knexfile.js is used by the Knex CLI to perform SQL operations. The seeds directory will contain our seeds or initial data which we can use for testing. Trust me when I say this, having this at hand, greatly simplifies development as you already have the data you want to work with.

并在根目录中创建一个名为“ seeds的新文件夹。 该knexfile.js使用由Knex CLI执行SQL操作。 seeds目录将包含我们的seeds或可用于测试的初始数据。 当我说完这句话时,请相信我,因为您已经有了要使用的数据,极大地简化了开发。

The structure should look something like the following:

该结构应类似于以下内容:

创建迁移 ( Creating the Migrations )

Let's create the actual migrations now. We'll create two tables users and birds. The users table will contain the username, password, name and email of the users; and the birds table will contain the listings of birds.

现在让我们创建实际的迁移。 我们将创建两个表usersbirdsusers表将包含用户名,密码,用户名和电子邮件; birds表将包含小鸟列表。

Create a new migration with knex migrate:make Datastructure to create a new migration file. It'll look something like 20161211185139_Datastructure.js. In your favorite text editor, open it and you'll see something like:

使用knex migrate:make Datastructure创建一个新的迁移,以创建一个新的迁移文件。 看起来像20161211185139_Datastructure.js 。 在您喜欢的文本编辑器中,将其打开,您将看到类似以下内容的内容:

exports.up = function(knex, Promise) {
};

exports.down = function(knex, Promise) {
};

The up function is executed when you migrate a database for this, and the down function is executed when you rollback.

为此,在迁移数据库时将执行up函数,而在回滚时将执行down函数。

Add the following to the up function:

将以下内容添加到up函数中:

exports.up = function(knex, Promise) {

    return knex
            .schema
            .createTable( 'users', function( usersTable ) {

                // Primary Key
                usersTable.increments();

                // Data
                usersTable.string( 'name', 50 ).notNullable();
                usersTable.string( 'username', 50 ).notNullable().unique();
                usersTable.string( 'email', 250 ).notNullable().unique();
                usersTable.string( 'password', 128 ).notNullable();
                usersTable.string( 'guid', 50 ).notNullable().unique();

                usersTable.timestamp( 'created_at' ).notNullable();

            } )

            .createTable( 'birds', function( birdsTable ) {

                // Primary Key
                birdsTable.increments();
                birdsTable.string( 'owner', 36 ).references( 'guid' ).inTable( 'users' );

                // Data
                // Each chainable method creates a column of the given type with the chained constraints. For example, in the line below, we create a column named `name` which has a maximum length of 250 characters, is of type string (VARCHAR) and is not nullable. 
                birdsTable.string( 'name', 250 ).notNullable();
                birdsTable.string( 'species', 250 ).notNullable();
                birdsTable.string( 'picture_url', 250 ).notNullable();
                birdsTable.string( 'guid', 36 ).notNullable().unique();
                birdsTable.boolean( 'isPublic' ).notNullable().defaultTo( true );

                birdsTable.timestamp( 'created_at' ).notNullable();

            } );

};

The chain .references(...) is used to create a composite primary key. This is done to ensure that we know who owns which listing.

.references(...)用于创建复合主键。 这样做是为了确保我们知道谁拥有哪个列表。

In the down function, add the following:

down功能中,添加以下内容:

exports.down = function(knex, Promise) {

    // We use `...ifExists` because we're not sure if the table's there. Honestly, this is just a safety measure. 
    return knex
        .schema
            .dropTableIfExists( 'birds' )
            .dropTableIfExists( 'users' );

};

Remember to drop a referencing table first; i.e., a table which uses field-referencing. In this case, birds had referenced guid in the table users, and so, we remove birds before we remove users as doing it the other way round will throw an error.

记住先删除一个引用表; 即使用字段引用的表。 在这种情况下, birds已经在表users引用了guid ,因此,我们先删除birds然后再删除users因为这样做反过来会引发错误。

Let's run this migration with knex migrate:latest.

让我们使用knex migrate:latest运行此迁移。

If all goes well, you should see:

如果一切顺利,您应该看到:

Now, if you head over to phpMyAdmin and check your database, you should see something like the following:

现在,如果您转到phpMyAdmin并检查数据库,则应该看到类似以下内容的内容:

Looks great to me. We'll now create some seed files by running knex seed:make 01_Users. Remember that the seed files are executed in the order of their file name, and so, if you execute the seed for the birds table first, the key constraint (reference) to guid in user will fail because the latter doesn't exist.

对我来说很棒。 现在,我们通过运行knex seed:make 01_Users创建一些种子文件。 请记住,种子文件是按照文件名的顺序执行的,因此,如果您首先为birds表执行种子,则对user guid的键约束(引用)将失败,因为后者不存在。

Under the seed folder, you should now see a new file titled 01_Users.js; open it, and replace the code with the following:

seed文件夹下,您现在应该看到一个名为01_Users.js的新文件; 打开它,并用以下代码替换代码:

exports.seed = function seed( knex, Promise ) {

    var tableName = 'users';

    var rows = [

        // You are free to add as many rows as you feel like in this array. Make sure that they're an object containing the following fields:
        {
            name: 'Shreyansh Pandey',
            username: 'labsvisual',
            password: 'password',
            email: 'me@isomr.co',
            guid: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
        },

    ];

    return knex( tableName )
        // Empty the table (DELETE)
        .del()
        .then( function() {
            return knex.insert( rows ).into( tableName );
        });

};

The code is self-explanatory, so I wouldn't bother going deeper. Similarly, let's create a sample bird migration with: knex seed:make 02_Birds and replacing the file with the following code:

该代码是不言自明的,因此我不会再深入研究。 同样,让我们使用以下示例创建一个鸟类迁移示例: knex seed:make 02_Birds并使用以下代码替换文件:

exports.seed = function seed( knex, Promise ) {

    var tableName = 'birds';

    var rows = [

        {
            owner: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
            species: 'Columbidae',
            name: 'Pigeon',
            picture_url: 'http://pngimg.com/upload/pigeon_PNG3423.png',
            guid: '4c8d84f1-9e41-4e78-a254-0a5680cd19d5',
            isPublic: true,
        },

        {
            owner: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
            species: 'Zenaida',
            name: 'Mourning dove',
            picture_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Mourning_Dove_2006.jpg/220px-Mourning_Dove_2006.jpg',
            guid: 'ddb8a136-6df4-4cf3-98c6-d29b9da4fbc6',
            isPublic: false,
        },

    ];

    return knex( tableName )
        .del()
        .then( function() {
            return knex.insert( rows ).into( tableName );
        });

};

Execute these seeds with knex seed:run and then open phpMyAdmin to be amazed.

使用knex seed:run执行这些种子,然后打开phpMyAdmin即可。

Beautiful. Now we can move onto creating the actual API.

美丽。 现在,我们可以继续创建实际的API。

启动API ( Starting the API )

JWT Authentication The authentication provider. In this case, we'll be using the super-simple and secure JSON Web Token strategy for authentication and authorization. Before moving forward, we need to do JWT-101.

JWT身份验证身份验证提供程序。 在这种情况下,我们将使用超级简单且安全的JSON Web令牌策略进行身份验证和授权。 在继续之前,我们需要做JWT-101

A JWT is in the form xxxxx.yyyyy.zzzzz with each of the sections having a specific name. We'll consider the token:

JWT的格式为xxxxx.yyyyy.zzzzz ,每个部分都有特定的名称。 我们将考虑令牌:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VybmFtZSI6ImxhYnN2aXN1YWwiLCJzY29wZSI6ImFkbWluIiwiaWF0IjoxNDgxMzg0NDQ3LCJleHAiOjE0ODEzODgwNDd9.
Y7B8rvGNmkwrSWMlb5e1Bqz0qnLuDLxerZmmdtg8ouo

The first block (xxxxx) is the header of the token and contains metadata such as the algorithm used for signature, etc. In our token, on decoding with UTF8 encoding, we get the following content

第一个块( xxxxx )是令牌的 ,并且包含元数据,例如用于签名的算法等。在我们的令牌中,使用UTF8编码进行解码时,我们得到以下内容

{
  "alg":"HS256",
  "typ":"JWT"
}

The next block (yyyyy) is the payload of the token and contains claims, expiration and creation metadata. Technically speaking, you can have whatever you want in this section. For our example, we get the following decoded content

下一个块( yyyyy )是令牌的有效载荷 ,其中包含claimsexpiration and creation metadata 。 从技术上讲,您可以在本节中拥有所需的任何东西。 对于我们的示例,我们得到以下解码内容

{
  "username": "labsvisual",
  "scope": "admin",
  "iat": 1481384447,
  "exp": 1481388047
}

As you can make out, this token was created for the user labsvisual who has admin scope and expires in 1h. Simple.

如您labsvisual ,此令牌是为具有admin labsvisual并在1h内到期的用户labsvisual创建的。 简单。

The last block (zzzzz) is the signature of the token and is calculated using HMAC256( ( base64Encode( header ) + '.' + base64Encode( payload ) ), secretKey ). You define the secret on the server; the library (jsonwebtoken) signs and verified the tokens using this secret. No matter what happens, make sure you do not leak this token anywhere as it will cause a problem in the authentication framework of your application.

最后一个块( zzzzz )是令牌的签名 ,并使用HMAC256( ( base64Encode( header ) + '.' + base64Encode( payload ) ), secretKey )计算得出。 您在服务器上定义机密; 库( jsonwebtoken )使用这个secret签名并验证了令牌。 无论发生什么情况,请确保不要在任何地方泄漏此令牌,因为这在应用程序的身份验证框架中引起问题。

A great tool to interactively debug and learn about JWT is its official website at JWT and the token debugger here. Below is a screenshot of the interactive JWT debugger in action.

一个伟大的工具,以交互方式调试和了解JWT是其在官方网站智威汤逊和令牌调试器在这里 。 下面是正在运行的交互式JWT调试器的屏幕截图。

Setting Up JWT Before we do anything, we need to tell hapi that we're going to use an authentication strategy (method) and so, it should load a couple of modules. Open up server.js and enter the following just after server.connection(...

设置JWT在执行任何操作之前,我们需要告诉hapi我们将使用身份验证策略(方法),因此,它应该加载几个模块。 打开server.js并在server.connection(...之后输入以下内容

// .register(...) registers a module within the instance of the API. The callback is then used to tell that the loaded module will be used as an authentication strategy. 
server.register( require( 'hapi-auth-jwt' ), ( err ) => {
    server.auth.strategy( 'token', 'jwt', {

        key: 'vZiYpmTzqXMp8PpYXKwqc9ShQ1UhyAfy',

        verifyOptions: {
            algorithms: [ 'HS256' ],
        }

    } );

} );

Here, we ask the server object to register a new module from the package hapi-jwt-auth; afterwhich, we register a new authentication strategy called token with the jwt scheme and the following options:

在这里,我们要求server对象从包hapi-jwt-auth注册一个新模块; 之后,我们使用jwt方案和以下选项注册一个称为token的新身份验证策略:

  • key - this is the private key which is used to sign and verify the JWT signatures;

    key -这是用于签署和验证JWT签名的私钥;
  • verifyOptions - we tell the library which algorithm to use for signature and verification; HMAC256 in this case.

    verifyOptions我们告诉库使用哪种算法进行签名和验证; 在这种情况下为HMAC256

You can also add validateFunc to the block which is used to validate the token provided. This is optional and is used when you have to do some other sort of verification in addition to the cryptographic verification provided by the library.

您还可以将validateFunc添加到用于验证所提供token的块。 这是可选的,并且在您必须执行除库提供的密码验证之外的其他其他类型的验证时使用。

src/server.js should look like the following now

src/server.js现在应该如下所示

import Hapi from 'hapi';

const server = new Hapi.Server();

server.connection( {
    port: 8080
} );

server.register( require( 'hapi-auth-jwt' ), ( err ) => {
    server.auth.strategy( 'token', 'jwt', {

        key: 'vZiYpmTzqXMp8PpYXKwqc9ShQ1UhyAfy',

        verifyOptions: {
            algorithms: [ 'HS256' ],
        }

    } );

} );

server.start( err => {

    if( err ) {

        // Fancy error handling here
        console.error( 'Error was handled!' );
        console.error( err );

    }

    console.log( `Server started at ${ server.info.uri }` );

} );

路线 (Routes)

Now, we can add the routes. Let's start by adding a simple route which gets all the public birds. Within src/server.js add the following route:

现在,我们可以添加路线了。 让我们从添加所有公共鸟类的简单路线开始。 在src/server.js添加以下路由:

server.route( {

    path: '/birds',
    method: 'GET',
    handler: ( request, reply ) => {    
    }

} );

Now, let's create a knex instance. Add a file knex.js within src and add the following code to it:

现在,让我们创建一个knex实例。 在src添加文件knex.js ,并向其中添加以下代码:

export default require( 'knex' )( {

    client: 'mysql',
    connection: {

        host: '192.168.33.10',

        user: 'birdbase',
        password: 'password',

        database: 'birdbase',
        charset: 'utf8',

    }

} );

Then import this file into your server.js by import Knex from './knex';. Now we are ready to utilize this awesome library.

然后通过import Knex from './knex';将该文件导入到server.js import Knex from './knex'; 。 现在我们准备好使用这个很棒的库了。

Let's select the name, picture_url and species for every public bird. In the handler for your route, add:

让我们为每只public鸟类选择namepicture_urlspecies 。 在您的路线处理程序中,添加:

...
handler: ( request, reply ) => {

    // In general, the Knex operation is like Knex('TABLE_NAME').where(...).chainable(...).then(...)
    const getOperation = Knex( 'birds' ).where( {

        isPublic: true

    } ).select( 'name', 'species', 'picture_url' ).then( ( results ) => {

        if( !results || results.length === 0 ) {

            reply( {

                error: true,
                errMessage: 'no public bird found',

            } );

        }

        reply( {

            dataCount: results.length,
            data: results,

        } );

    } ).catch( ( err ) => {

        reply( 'server-side error' );

    } );

}
...

The line = Knex('... tells Knex to use the birds database, and then builds the query where the field isPublic is set to true. Then fetches it using .select (which returns a promise) and then resolving the promise. The parameter results is an array of all the birds which match the criterion.

= Knex('...告诉Knex使用birds数据库,然后建立查询where外地isPublic设置为true ,然后使用它取出.select (它返回一个承诺),然后解决的承诺。该参数results是所有符合条件的鸟的数组。

Save the file and start the API server with npm start and then fire up your favourite API client. We'll use Paw.

保存文件并使用npm start API服务器,然后启动您喜欢的API客户端。 我们将使用Paw。

Pat yourself on the back if everything works as expected. Your src/server.js file should look like:

如果一切正常,请拍拍自己的背部。 您的src/server.js文件应如下所示:

import Hapi from 'hapi';
import Knex from './knex';

const server = new Hapi.Server();

server.connection( {
    port: 8080
} );

server.register( require( 'hapi-auth-jwt' ), ( err ) => {
    server.auth.strategy( 'token', 'jwt', {

        key: 'vZiYpmTzqXMp8PpYXKwqc9ShQ1UhyAfy',

        verifyOptions: {
            algorithms: [ 'HS256' ],
        }

    } );

} );

// --------------
// Routes
// --------------

server.route( {

    path: '/birds',
    method: 'GET',
    handler: ( request, reply ) => {

        const getOperation = Knex( 'birds' ).where( {

            isPublic: true

        } ).select( 'name', 'species', 'picture_url' ).then( ( results ) => {

            // The second one is just a redundant check, but let's be sure of everything.
            if( !results || results.length === 0 ) {

                reply( {

                    error: true,
                    errMessage: 'no public bird found',

                } );

            }

            reply( {

                dataCount: results.length,
                data: results,

            } );

        } ).catch( ( err ) => {

            reply( 'server-side error' );

        } );

    }

} );

server.start( err => {

    if( err ) {

        // Fancy error handling here
        console.error( 'Error was handled!' );
        console.error( err );

    }

    console.log( `Server started at ${ server.info.uri }` );

} );

Now, we'll continue by adding an auth route which will be used to authenticate the user. The logic here is very simple: check if the password of the payload is the same as the one in the database, and if so, create a new JWT token with the scope of the user's GUID which expires in 1h.

现在,我们将继续添加用于验证用户身份的身份auth路由。 这里的逻辑非常简单:检查有效负载的password是否与数据库中的password相同,如果是,则创建一个新的JWT令牌,其用户GUID的范围在1h内到期。

While updating a bird, we'll use a preRouteHandler to check if the current user owns the bird; if he does, then we'll allow the edit, otherwise, we'll throw a 403 error.

在更新bird ,我们将使用preRouteHandler来检查当前用户是否拥有该鸟; 如果他这样做,那么我们将允许编辑,否则,我们将抛出403错误。

验证路线 (Auth Route)

Let's create a POST route with

让我们创建一个POST路由

server.route( {

    path: '/auth',
    method: 'POST',
    handler: ( request, reply ) => {

        // This is a ES6 standard
        const { username, password } = request.payload;

        const getOperation = Knex( 'users' ).where( {

            // Equiv. to `username: username`
            username,

        } ).select( 'guid', 'password' ).then( ( results ) => {

        } ).catch( ( err ) => {

            reply( 'server-side error' );

        } );

    }

} );

The line const { username... decomposes the request.payload object and gets the named values (username and password in this case). This is the same as:

const { username...可分解request.payload对象并获取命名值(在这种情况下为usernamepassword )。 这与以下内容相同:

const username = request.payload.username;

Just another lovely example of why I love ES6.

这是为什么我喜欢ES6的另一个可爱示例。

Remember that the object request.payload contains all the content in a POST or a PUT request.

请记住,对象request.payload包含POSTPUT请求中的所有内容。

Let's continue.

让我们继续。

First we need to make sure that we select exactly one. Let's use the array deconstructor and then check if it's populated:

首先,我们需要确保选择正好一个 。 让我们使用数组解构函数,然后检查它是否已填充:

...
} ).select( 'guid', 'password' ).then( ( [ user ] ) => {
    if( !user ) {

        reply( {

            error: true,
            errMessage: 'the specified user was not found',

        } );

        // Force of habit. But most importantly, we don't want to wrap everything else in an `else` block; better is, just return the control.
        return;

    }
...

Simple enough. We check if the user exists and if not, we throw an error and exit out of the function. Let's finish this route:

很简单。 我们检查用户是否存在,如果不存在,则抛出错误并退出功能。 让我们完成以下路线:

...
// Honestly, this is VERY insecure. Use some salted-hashing algorithm and then compare it.
if( user.password === password ) {

    const token = jwt.sign( {

        // You can have anything you want here. ANYTHING. As we'll see in a bit, this decoded token is passed onto a request handler.
        username,
        scope: user.guid,

    }, 'vZiYpmTzqXMp8PpYXKwqc9ShQ1UhyAfy', {

        algorithm: 'HS256',
        expiresIn: '1h',

    } );

    reply( {

        token,
        scope: user.guid,

    } );

} else {

    reply( 'incorrect password' );

}

The function jwt.sign( payload, key, [ options ] ) signs the payload and gives a JWT which we then transmit to the user. Save this file and start your server, and add a new request to your client.

函数jwt.sign( payload, key, [ options ] )对有效负载进行签名,并提供一个JWT,然后将其传输给用户。 保存此文件并启动服务器,然后向客户端添加新请求。

Try changing the username and the password to see what kind of response you get.

尝试更改用户名和密码,以查看得到什么样的响应。

So far, so good. Now we'll add a method to create a bird, and a method to update a bird. Let's start.

到目前为止,一切都很好。 现在,我们将添加创建鸟的方法和更新鸟的方法。 开始吧。

创建鸟 (Create a Bird)

Create a POST route at /birds and add the empty handler function.

/birds创建一个POST路由,并添加空的处理函数。

server.route( {

    path: '/birds',
    method: 'POST',
    handler: ( request, reply ) => {

        const { bird } = request.payload;

    }

} );

We expect to have a payload as bird which is an object containing all the information about the bird. Let's add the code to insert this into our database.

我们希望有一个有效载荷作为bird ,它是一个包含有关鸟的所有信息的对象。 让我们添加代码以将其插入到我们的数据库中。

Before anything, we need to tell Hapi.js that this route is protected by authentication. To do so, add the following after method in the route:

首先,我们需要告诉Hapi.js此路由受身份验证保护。 为此,在路由中添加以下after method

...
method: 'POST',
config: {

    auth: {

        strategy: 'token',

    }

},
...

This tells Hapi.js that we'll be using a registered authentication strategy for our route.

这告诉Hapi.js,我们将对路由使用注册的身份验证策略。

But, for this to work properly, we need to refractor the code a little bit. Let's add a new file routes.js containing all the routes. Something like:

但是,为了使其正常工作,我们需要稍微延迟一下代码。 让我们添加一个包含所有路由的新文件routes.js 。 就像是:

import Knex from './knex';
import jwt from 'jsonwebtoken';

// The idea here is simple: export an array which can be then iterated over and each route can be attached. 
const routes = [

    {

        path: '/birds',
        method: 'GET',
        handler: ( request, reply ) => {

            const getOperation = Knex( 'birds' ).where( {

                isPublic: true
...
export default routes;

Then, in src/server.js, we'll import the routes array as import routes from './routes'; and then within server.register(... we'll add the following bit to register all the routes:

然后,在src/server.js ,我们将路由数组作为import routes from './routes'; 然后在server.register(... ,添加以下位以注册所有路由:

...
    routes.forEach( ( route ) => {

        console.log( `attaching ${ route.path }` );
        server.route( route );

    } );

The src/server.js file becomes something like:

src/server.js文件变为:

import Hapi from 'hapi';
import routes from './routes';

const server = new Hapi.Server();

server.connection( {
    port: 8080
} );

server.register( require( 'hapi-auth-jwt' ), ( err ) => {

    if( !err ) {
        console.log( 'registered authentication provider' );
    }

    server.auth.strategy( 'token', 'jwt', {

        key: 'vZiYpmTzqXMp8PpYXKwqc9ShQ1UhyAfy',

        verifyOptions: {
            algorithms: [ 'HS256' ]
        }

    } );

    // We move this in the callback because we want to make sure that the authentication module has loaded before we attach the routes. It will throw an error, otherwise. 
    routes.forEach( ( route ) => {

        console.log( `attaching ${ route.path }` );
        server.route( route );

    } );

} );


server.start( err => {

    if( err ) {

        // Fancy error handling here
        console.error( 'Error was handled!' );
        console.error( err );

    }

    console.log( `Server started at ${ server.info.uri }` );

} );

With that done, let's move on.

完成后,让我们继续。

Now, we need to add a bird to the birds database. We'll do this using Knex. Add the following bit immediately after const { bird } = request.payload; in your route within src/routes.js.

现在,我们需要将bird添加到birds数据库。 我们将使用Knex进行此操作。 在const { bird } = request.payload;之后立即添加以下位const { bird } = request.payload;src/routes.js中的路由中。

We'll install a package to generate GUID's called node-uuid with npm i --save node-uuid and then import it into our routes file as import GUID from 'node-uuid';.

我们将安装一个软件包,用npm i --save node-uuid生成GUID,称为node-uuid ,然后将其作为import GUID from 'node-uuid';导入到我们的路由文件中import GUID from 'node-uuid';

const guid = GUID.v4();

const insertOperation = Knex( 'birds' ).insert( {

    owner: request.auth.credentials.scope,
    name: bird.name,
    species: bird.species,
    picture_url: bird.picture_url,
    guid,

} ).then( ( res ) => {

    reply( {

        data: guid,
        message: 'successfully created bird'

    } );

} ).catch( ( err ) => {

    reply( 'server-side error' );

} );

The code is pretty self-explanatory apart from this interesting object request.auth.credentials. Well, after the verification is done, the authentication handler passes on the decoded token to this credentials object. If you do a console.log( request.auth.credentials ); you'll see something like:

除了这个有趣的对象request.auth.credentials之外,代码是很容易解释的。 好了,验证完成后,身份验证处理程序将解码的令牌传递给此credentials对象。 如果您执行console.log( request.auth.credentials ); 您会看到类似以下内容:

{
    username: 'labsvisual',
    scope: 'f03ede7c-b121-4112-bcc7-130a3e87988c',
    iat: 1481546651,
    exp: 1481550251
}

From here, we can grab on to the GUID for the user and pass it on as the owner in the database. Simple.

从这里,我们可以抓住用户的GUID ,并将其作为数据库的owner传递给GUID 。 简单。

Fire up the server and then add a couple of requests. Remember to add the Authorization header in the following format: Authorization: Bearer <JWT> where <JWT> is your generated JWT in the /auth route.

启动服务器,然后添加几个请求。 切记以以下格式添加Authorization标头: Authorization: Bearer <JWT>其中<JWT>是您在/auth路由中生成的JWT。

Let's check the database:

让我们检查数据库:

And now let's get a listing of all public birds:

现在让我们列出所有公共鸟类:

更新鸟类 (Update Birds)

With that in place, we can create our last route: PUT at /birds/:guid where we can update the bird with the GUID guid. I'll just copy and paste the POST route and make the changes as and when required.

完成此操作后,我们可以创建最后一条路由:在/birds/:guid处放置PUT在此处可以使用GUID guid更新鸟。 我将只复制并粘贴POST路由,并在需要时进行更改。

For starters, let's change the method to PUT and the route to /bird/{birdGuid}. We can access this birdGuid from request.params. After the changes, it should look something like:

首先,让我们将method更改为PUT ,并将route更改为/bird/{birdGuid} 。 我们可以从request.params访问此birdGuid 。 更改后,它应类似于:

{

        path: '/birds/{birdGuid}',
        method: 'PUT',
...

Now, we want to verify that the current user has rights to the bird he's trying to edit. For that, we need to validate if the bird associated with birdGuid has the same owner as the scope of the authorization token passed. As illustrated before, we can access the token's GUID from request.auth.credentials and then the scope property in that. However, let's add a route prerequisite: this function has the same signature as the handler of a route and is executed before the control is passed to the handler. Really useful in cases like this where you need to do some sort of verification. Also note that we can safely assume that when this function is being executed, some user has passed some valid JWT in the authorization header; had they not done that, the hapi-jwt-auth library would've thrown a 401 error.

现在,我们要验证当前用户是否拥有他要编辑的鸟的权限。 为此,我们需要验证与birdGuid关联的鸟birdGuid具有与所传递的授权令牌的范围相同的owner 。 如前所述,我们可以从request.auth.credentials访问令牌的GUID ,然后在其中访问scope属性。 但是,让我们添加一个路由先决条件:该函数具有与路由handler相同的签名,并且将控件传递给handler 之前执行。 在需要进行某种验证的情况下,此方法确实很有用。 还要注意,我们可以放心地假设,当执行此函数时,某些用户已在授权标头中传递了一些有效的JWT。 如果他们不这样做,那么hapi-jwt-auth库将抛出401错误。

The route should be something like:

路线应类似于:

config: {
    ...
    pre: [
        {
            method: ( request, reply ) => {

            }
        }
    ]
...

The pre configuration block is an array which contains objects with a key method which is linked to a function.

pre配置块是一个数组,其中包含带有键method对象,该键method链接到函数。

We'll first pull out all the values from the objects and store them:

我们将首先从对象中提取所有值并存储它们:

...
const { birdGuid } = request.params
         , { scope }    = request.auth.credentials;
...

Next, let's do a select operation on the database where we select the bird from the GUID provided; we'll just select the owner column of the bird as that's all we need for verification:

接下来,让我们对数据库进行select操作,从提供的GUID选择鸟类。 我们只需要选择鸟的owner列即可,这是我们验证所需要的:

...
const getOperation = Knex( 'birds' ).where( {

    guid: birdGuid,

} ).select( 'owner' ).then( ( [ result ] ) => {

} );
...

Brilliant. Now, we have selected just one bird with [ result ] and we can work on that.

辉煌。 现在,我们只选择[ result ]一只鸟,我们可以进行研究。

Let's start by verifying that we actually have a bird with the specified GUID; if we do not, then we'll take over the request and send a custom reply. reply().takeover() ends the reply chain with the last response you give, and hence, does not let the handler get envoked.

让我们首先验证一下我们是否确实有一个带有指定GUID的鸟。 如果不这样做,那么我们将接管请求并发送自定义回复。 reply().takeover()以您给出的最后一个响应结束回复链,因此不会让handler被调用。

...
if( !result ) {

    reply( {

        error: true,
        errMessage: `the bird with id ${ birdGuid } was not found`

    } ).takeover();

}
...

Next, let's check if the scope of the current token allows the user to modify the bird with the guid.

接下来,让我们检查当前令牌的scope是否允许用户使用guid修改鸟。

...
if( result.owner !== scope ) {

    reply( {

        error: true,
        errMessage: `the bird with id ${ birdGuid } is not in the current scope`

    } ).takeover();

}
...

If not, we'll just let the reply chain continue:

如果没有,我们就让reply链继续:

...
return reply.continue();
...

After you're done, this method should be something like:

完成后,此方法应类似于:

...
method: ( request, reply ) => {

    const { birdGuid } = request.params
        , { scope }    = request.auth.credentials;

    const getOperation = Knex( 'birds' ).where( {

        guid: birdGuid,

    } ).select( 'owner' ).then( ( [ result ] ) => {

        if( !result ) {

            reply( {

                error: true,
                errMessage: `the bird with id ${ birdGuid } was not found`

            } ).takeover();

        }

        if( result.owner !== scope ) {

            reply( {

                error: true,
                errMessage: `the bird with id ${ birdGuid } is not in the current scope`

            } ).takeover();

        }

        return reply.continue();

    } );

}
...

Lastly, let's add the update chain to the handler so we can UPDATE the current bird with the information.

最后,让我们将update链添加到handler以便我们可以使用信息来UPDATE当前鸟。

...
handler: ( request, reply ) => {

    const { birdGuid } = request.params
        , { bird }     = request.payload;

    const insertOperation = Knex( 'birds' ).where( {

        guid: birdGuid,

    } ).update( {

        name: bird.name,
        species: bird.species,
        picture_url: bird.picture_url,
        isPublic: bird.isPublic,

    } ).then( ( res ) => {

        reply( {

            message: 'successfully updated bird'

        } );

    } ).catch( ( err ) => {

        reply( 'server-side error' );

    } );

}
...

The code is self-explanatory. So, we'll just continue. Now, fire up the server and add a new request.

该代码是不言自明的。 因此,我们将继续。 现在,启动服务器并添加新请求。

Let's change the isPublic parameter:

让我们更改isPublic参数:

You can also try giving incorrect birdGuid and seeing what happens:

您也可以尝试提供错误的birdGuid并查看会发生什么:

结论 ( Conclusion )

In the end, we learned quite a few things here. We went from just being a beginner in Hapi.js to creating a fully blown API in Hapi with Authentication, MySQL DB, etc. The code is available here; if you find some errors, or have some suggestions, please be sure to tell me. if you have any questions, be sure to throw them in the comment box below. Until next time! Cheers!

最后,我们在这里学到了很多东西。 我们从刚刚正在Hapi.js创造的哈啤与认证,MySQL数据库等完全吹成API的代码可以从初学者来到这里 ; 如果您发现某些错误或有任何建议,请务必告诉我。 如果您有任何疑问,请务必将其放在下面的评论框中。 直到下一次! 干杯!

翻译自: https://scotch.io/tutorials/making-a-restful-api-with-hapi-js

hapi.js

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP