Skip to content

Latest commit

 

History

History
1346 lines (1026 loc) · 50.8 KB

File metadata and controls

1346 lines (1026 loc) · 50.8 KB

七、改进 restfulweb 服务

在上一章中,我们在 Lumen 中创建了 RESTfulWeb 服务,并确定了一些缺少的元素或需要的改进。在本章中,我们将致力于改进这一点,并完成一些缺失的元素。我们将在弥补漏洞和代码质量方面改进某些元素。

我们将在本章中介绍以下主题,以改进 RESTful web 服务:

  • Dingo,简化 RESTful API 开发:
    • 安装和配置 Dingo API 包
    • 简化路线
    • API 版本控制
    • 速率限制
    • 内部请求
  • 身份验证和中间件
  • 变形金刚
  • 需要加密:
    • SSL,不同的选项
  • 总结

Dingo,简化 RESTful API 开发

是的,你听对了。我没说宾果。这是野狗。实际上,DingoAPI 是一个用于 Laravel 和 Lumen 的包,它使开发 RESTful web 服务变得更加简单。它提供了许多开箱即用的特性,其中许多是我们在上一章中看到的。其中许多特性将使我们现有的代码更好、更易于理解和维护。您可以在查看 Dingo API 包 https://github.com/dingo/api

让我们先安装它,在使用它们的同时,我们将继续关注它的优点和特性。

安装和配置

只需通过 composer 安装即可:

composer require dingo/api:1.0.x@dev

可能你想知道这是什么。因此,野狗文件上说:

At this time, the package is still in a developmental stage and as such, does not have a stable release. You may need to set your minimum stability to dev.

如果您仍然不确定为什么需要设置最小稳定性,那么这是因为每个包的默认最小稳定性设置为stable。因此,如果您依赖于dev包,那么应该明确指定它,否则它可能不会安装它,因为最低稳定性与包的实际稳定性状态不匹配。

安装后,您需要注册它。转到bootstrap/app.php并将此语句放在return $app;之前的文件中:

$app->register(Dingo\Api\Provider\LumenServiceProvider::class);

完成此操作后,您需要在.env文件中设置一些变量。将它们添加到.env文件的末尾:

API_PREFIX=api
API_VERSION=v1
API_DEBUG=true
API_NAME="Blog API"
API_DEFAULT_FORMAT=json

配置是不言自明的。现在,让我们继续前进。

简化路线

如果您查看我们放置路由的routes/web.php文件,您可以看到我们为 post 和 comment 端点编写了 54 行代码。有了 DingoAPI,我们可以用 10 行代码替换这 54 行代码,而且它会更干净。那我们就这么做吧。以下是您的routes/web.php文件的外观:

<?php

/*
|----------------------------------------------------------------
| Application Routes
|----------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function ($api) {

    $api->resource('posts', "App\Http\Controllers\PostController");
    $api->resource('comments', "App\Http\Controllers\CommentController", [
        'except' => ['store', 'index']
    ]); 
 $api->post('posts/{id}/comments', 'App\Http\Controllers\CommentController@store');

 $api->get('posts/{id}/comments', 'App\Http\Controllers\CommentController@index'); });

$app->get('/', function () use ($app) {
 return $app->version();
}); 

如您所见,我们刚刚在$api中获得了路由器的对象。然而,这是 Dingo API 路由器,而不是 Lumen 的默认路由器。正如您所看到的,它有我们想要的resource()方法,我们可以在except数组中提到不需要的方法。因此,总体而言,我们的路线现在大大简化了。

要查看应用程序的确切路由,请运行以下命令:

php artisan route:list

API 版本控制

您可能已经注意到,在路由文件的前面代码示例中,我们已经提到了 API 版本为v1。这是因为 API 版本控制很重要,而 Dingo 为我们提供了这样做的工具。为不同版本的不同端点提供服务非常有用。您可以拥有另一个版本组,并且可以使用不同的内容为同一端点提供服务。如果不同版本下有相同的端点,那么它将选择您的.env文件中提到的版本。

不过,最好在 URI 中有一个版本。为此,我们可以简单地使用以下前缀:

$api->version('v1', ['prefix' => 'api/v1'], function ($api) {

     $api->resource('posts', "App\Http\Controllers\PostController");
     $api->resource('comments', "App\Http\Controllers\CommentController", [
 'except' => ['store', 'index']
 ]);

     $api->post('posts/{id}/comments', 'App\Http\Controllers\CommentController@store');

     $api->get('posts/{id}/comments', 'App\Http\Controllers\CommentController@index');
});

现在,我们的路由将在 URI 中包含版本信息。这是一种推荐的方法。因为如果有人正在使用版本 1,并且我们将在版本 2 中更改某些内容,那么如果使用版本 1 的客户机在其请求中明确指定版本号,则不会受到影响。因此,我们的端点 URL 如下所示:

http://localhost:8000/api/v1/posts
http://localhost:8000/api/v1/posts/1
http://localhost:8000/api/v1/posts/1/comments

但是,请注意,如果我们的 URI 和路由中有版本,那么最好在控制器中保持该版本实际适用。否则,版本实现将非常有限。为此,我们应该为控制器提供一个基于版本的命名空间。在我们的控制器中(包括PostControllerCommentController,名称空间将更改为以下代码行:

namespace App\Http\Controllers\V1;

现在,控制器目录结构也应该与名称空间匹配。那么,让我们在Controllers目录中创建一个名为V1的目录,并将控制器移动到app\Http\Controllers**\V1**目录中。当我们有下一个版本时,我们将在app\Http\Controllers中创建另一个名为V2的目录,并在其中添加新的控制器。它还将产生一个新的名称空间App\Http\Controllers**\V2**。随着名称空间和目录的更改,routes/web.php中控制器的路径也需要相应地更改。

通过此更改,您很可能会看到以下错误:

Class 'App\Http\Controllers\V1\Controller' not found

因此,要么将 controllers 目录中的Controller.php移动到V1目录,要么简单地使用完整的名称空间访问它,例如\App\Http\Controllers\Controller,如下所示:

class PostController extends \App\Http\Controllers\Controller
{..

这取决于你怎么做。

速率限制

速率限制也称为节流。这意味着在特定的时间间隔内,特定客户机能够访问 API 端点的时间应该有一个限制。要启用它,我们必须启用api.throttling中间件。您可以在所有管线或特定管线上应用节流。您只需在该特定路由上应用中间件,如下所示。在本例中,我们希望为所有端点启用它,因此让我们将其放入一个版本组:

$api->version('v1', ['middleware' => 'api.throttle','prefix' => 'api/v1'], 
function ($api) {
 $api->resource('posts', "App\Http\Controllers\V1\PostController");
 $api->resource('comments', "App\Http\Controllers\V1\CommentController", [
 'except' => ['store', 'index']
 ]);

$api->post('posts/{id}/comments', 'App\Http\V1\Controllers\CommentController@store');
$api->get('posts/{id}/comments', 'App\Http\V1\Controllers\CommentController@index');

});

为了简单起见,让我们改变一下路线。与使用每个控制器的名称指定名称空间不同,我们只需在版本组中使用名称空间,如下所示:

$api->version('v1', ['middleware' => 'api.throttle', 'prefix' => 'api/v1', **namespace => "App\HttpControllers\V1"** **]**

现在,我们可以简单地将其从控制器路径中删除。

您还可以提及以分钟为单位的限制和时间间隔:

$api->version('v1', [
    'middleware' => 'api.throttle',
 'limit' => 100,
    'expires' => 5,
    'prefix' => 'api/v1',
    'namespace' => 'App\Http\Controllers\V1'
    ],
  function ($api) {

    $api->resource('posts', "PostController");
    $api->resource('comments', "CommentController", [
        'except' => ['store', 'index']
    ]);

    $api->post('posts/{id}/comments', 'CommentController@store');
    $api->get('posts/{id}/comments', 'CommentController@index');

});

这里,expires是时间间隔,limit是一条路由可以访问的次数。

内部请求

我们主要是制作一个 API,由外部客户端(而不是同一个应用程序)从外部作为 web 服务进行访问。但是,有时,我们需要在同一个应用程序中发出内部请求,并希望数据的格式与返回给外部客户机的格式相同。

假设您希望 API 在PostController中提供注释数据,因为它返回响应而不是内部函数调用。当邮递员或其他客户机点击/api/posts/{postId}/comments端点时,我们需要与该端点返回的数据相同的数据。在这种情况下,dingoapi 包可以帮助我们。这是多么简单:

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Dingo\Api\Routing\Helpers;

class PostController extends Controller
{
 use Helpers;

    public function __construct(\App\Post $post)
    {
        $this->post = $post;
    }
....
/**
 * Display the specified resource.
 *
 * @param  int  $id
 * @return \Illuminate\Http\Response
 */
public function show($id)
{
 $comments = $this->api->get("api/posts/$id/comments");

 $post = $this->post->find($id);
....
 }
....
}

前面代码段中的粗体语句是不同的,它有助于发出内部请求。正如您所说,我们已经向端点发出了基于GET的请求:

 $comments = $this->api->get("api/posts/$id/comments");

我们也可以使用以下不同的方法将其设置为基于POST的请求:

$this->api->post("api/v1/posts/$id/comments", ['comment' => 'a nice post']);

响应

dingoapi 包为不同类型的响应提供了更多的支持。由于我们不会详细介绍 Dingo API 提供的每一项内容,您可以在的文档中查看 https://github.com/dingo/api/wiki/Responses

但是,在本章后面,我们将详细介绍响应和格式。

我们还将在其他方面使用 Dingo API 包,但现在,让我们转向其他概念,我们将继续并肩使用 Dingo API 包。

身份验证和中间件

我们已经多次讨论过,对于 RESTful web 服务,会话是通过存储在客户端的身份验证令牌来维护的。因此,服务器可以查找身份验证令牌,并可以找到存储在服务器上的用户会话。

有几种方法可以生成令牌。在本例中,我们将使用JWTJSON Web 令牌。如jwt.io所述:

JSON Web 令牌是一种开放的行业标准 RFC 7519 方法,用于在双方之间安全地表示声明。

我们将不详细介绍 JWT,因为 JWT 是一种在双方(在我们的例子中是客户端和服务器)之间传输信息的方法,因为 JWT 可以用于多种用途。相反,我们将使用它作为访问/身份验证令牌来维护无状态会话。因此,我们将坚持 JWT 的要求。我们需要它来维护会话以进行身份验证,而这也是 DingoAPI 包也将帮助我们的。

事实上,在编写本书时,dingoapi 支持三个身份验证提供者。默认情况下,启用 HTTP 基本身份验证。另外两个是 JWT Auth 和 OAuth 2.0。我们将使用 jwtauth。

JWT 身份验证设置

Dingo API 用于集成 JWT 身份验证的包可在中找到 https://github.com/tymondesigns/jwt-auth.



设置 JWT Auth 有两种方法:

  1. 我们可以简单地按照 JWT Auth 包的说明,手动将其配置为与 Dingo 一起使用,并手动逐个修复问题。
  2. 我们可以简单地安装另一个包,帮助我们安装和设置 dingoapi 和 jwtauth 以及一些基本配置。

在这里,我们将看到两个方面。然而,使用手动方式时,由于不同包装的不同版本和流明本身,可能会产生歧义。因此,尽管我将以手动方式展示,但我建议您使用集成包,这样您就不需要在低级别手动处理所有事情。我将向您展示手动方式,让您了解下面包含的内容。

手动方式

让我们按照安装页面上的说明安装软件包 https://github.com/tymondesigns/jwt-auth/wiki/Installation.

首先,我们需要安装 JWT Auth 包:

 composer require tymon/jwt-auth 1.0.0-beta.3

请注意,此版本适用于 Laravel 5.3。对于旧版本,您可能需要使用不同版本的 JWT Auth 包,最可能的版本是 0.5。

要在bootstrap/app.php文件中注册服务提供商,请添加以下代码行:

$app->register(Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class);

然后,在同一bootstrap/app.php文件中添加这两个类别名:

class_alias('Tymon\JWTAuth\Facades\JWTAuth', 'JWTAuth');
class_alias('Tymon\JWTAuth\Facades\JWTFactory', 'JWTFactory');

然后,您需要生成一个随机密钥,用于对令牌进行签名。要执行此操作,请运行以下命令:

php artisan jwt:generate

这将生成一个随机密钥。

You might see some error as shown here:

[Illuminate\Contracts\Container\BindingResolutionException] Unresolvable dependency resolving [Parameter #0 [ <required> $app ]] in class Illuminate\Cache\CacheManager

In this case, add the following lines in bootstrap/app.php right after $app->withEloquent();. So, it will fix the problem and you can try generating a random key:

$app->alias('cache', 'Illuminate\Cache\CacheManager');
$app->alias('auth', 'Illuminate\Auth\AuthManager');

但是,您可能想知道这个随机键将设置在哪里。实际上,有些封装不是为流明而设计的,需要一个更像 Laravel 的结构。tymondesigns/jwt-auth包就是其中之一。它需要的是一种发布配置的方法。虽然 Lumen 没有针对不同软件包的单独配置文件,但我们需要它,我们可以简单地让 Lumen 为这个软件包提供这样一个config文件。为此,如果您在app/目录下没有helpers.php,则创建它并添加以下内容:

<?php
if ( ! function_exists('config_path'))
{
    /**
     * Get the configuration path.
     *
     * @param  string $path
     * @return string
     */
    function config_path($path = '')
    {
        return app()->basePath() . '/config' . ($path ? '/' . $path : $path);
    }
}

然后,将helpers.php添加到自动加载数组中的composer.json

"autoload": {
  "psr-4": {
    "App\\": "app/"
  },
  "files": [
    "app/helpers.php"
  ]
},

运行以下命令:

composer dump-autoload

完成后,运行以下命令:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"

此时,您将看到另一个错误,即:

[Symfony\Component\Console\Exception\CommandNotFoundException]
 There are no commands defined in the "vendor" namespace.

别担心;这是意料之中的事。这是因为 Lumen 没有现成的vendor:publish命令。因此,我们需要为该命令安装一个小程序包:

composer require laravelista/lumen-vendor-publish

由于此命令将有一个新命令,为了使用该命令,我们需要将以下内容放入app/Console/Kernel.php中的$commands数组中。

现在,再次尝试运行相同的命令,如下所示:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"

这一次,您将看到如下内容:

Copied File [/vendor/tymon/jwt-auth/src/config/config.php] To [/config/jwt.php]
Publishing complete for tag []!

现在,我们有了blog/config/jwt.php,我们可以在这个文件中存储jwt-auth包相关的配置。

我们需要做的第一件事是重新运行此命令以设置随机密钥签名:

php artisan jwt:generate

这一次,您可以在返回数组的config/jwt.php文件中看到该键的设置:

'secret' => env('JWT_SECRET', 'RusJ3fiLAp4DmUNNzqGpC7IcQI8bfar7'),

接下来,您需要进行配置,如所示 https://github.com/tymondesigns/jwt-auth/wiki/Configuration

但是,您也可以将config/jwt.php中的其他设置保留为默认设置。

下一步是告诉 dingoapi 使用 JWT 作为身份验证方法。所以在bootstrap/app.php中加上:

app('Dingo\Api\Auth\Auth')->extend('jwt', function ($app) {
 return new Dingo\Api\Auth\Provider\JWT($app['Tymon\JWTAuth\JWTAuth']);
});

根据 jwtauth 文档,我们主要完成了配置,但请注意,您可能会遇到与版本相关的小问题。如果您使用的版本早于 Lumen 5.3,请注意,根据不同的 Laravel 版本,需要不同版本的 JWT Auth。对于版本 5.2,您应该使用 JWT Auth 版本 0.5。因此,如果您在 Laravel 5.2 之前的版本中仍然出现任何错误,请注意,错误可能是由于版本差异造成的,因此您必须在 Internet 上搜索。

正如您所看到的,为了同时使用两个包来实现某些功能,我们必须在配置上花费一些时间,正如在最后几个步骤中所建议的那样。即使如此,由于版本差异也有可能出现错误。因此,一种简单易行的方法是不手动安装 dingoapi 包和 jwtauth 包。还有另一个安装包,它将安装 Dingo API 包、Lumen 生成器、CORS跨源资源共享)支持和 JWTAuth,并使其可以在没有太多配置的情况下使用。现在,让我们看看这个。

通过 Lumen JWT 身份验证集成包的更简单方法

一个更简单的方法是自己安装 Dingo API 包和 JWT Auth,只需安装https://packagist.org/packages/krisanalfa/lumen-dingo-adapter.

它将在基于流明的应用程序中添加 Dingo 和 JWT。只需安装此软件包:

composer require krisanalfa/lumen-dingo-adapter

然后,在bootstrap/app.php中添加以下代码行:

$app->register(Zeek\LumenDingoAdapter\Providers\LumenDingoAdapterServiceProvider::class);

现在,通过这种方式,我们正在使用这个LumenDingoAdapter包,因此这里是我们将使用的bootstrap/app.php文件,以便您可以将其与您的进行比较:

<?php

require_once __DIR__.'/../vendor/autoload.php';

try {
    (new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
    //
}

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/

$app = new Laravel\Lumen\Application(
    realpath(__DIR__.'/../')
);

 $app->withFacades();

 $app->withEloquent();

/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

 $app->register(Zeek\LumenDingoAdapter\Providers\LumenDingoAdapterServiceProvider::class); 

/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/

// $app->middleware([
//    App\Http\Middleware\ExampleMiddleware::class
// ]);

// $app->routeMiddleware([
//     'auth' => App\Http\Middleware\Authenticate::class,
// ]);

/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/

// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);

/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/

$app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
    require __DIR__.'/../routes/web.php';
});

return $app;

如果您想知道这$app->withFacades()到底是做什么的,那么请注意,这将在应用程序中启用 facades。Facades 是一种设计模式,用于将复杂的事物抽象化,同时提供简化的交互界面。在 Lumen 中,正如 Laravel 文档所述:Facades 为应用程序服务容器中可用的类提供“静态”接口。

使用 facades 的好处是它提供了令人难忘的语法。我们不会经常使用 Facades,并且会尽量避免使用 Facades,因为我们更喜欢依赖注入,而不是依赖注入。然而,一些软件包可能会使用 facades,所以为了让它们工作,我们启用了 facades。

认证

现在,我们可以使用api.auth中间件来保护我们的端点。该中间件检查用户身份验证并从 JWT 获取用户。但是,第一件事是让用户登录,基于该用户信息创建令牌,并将签名的令牌返回给客户端。

为了使身份验证工作,我们首先需要创建一个与身份验证相关的控制器。该控制器不仅基于用户登录创建令牌,还将使用户令牌过期并刷新令牌。为了做到这一点,我们可以将这个开源软件AuthController放在 app/Http/Controllers/Auth/目录中 https://github.com/Haafiz/REST-API-for-basic-RPG/blob/master/app/Http/Controllers/Auth/AuthController.php.

我想告诉大家,我们用于AuthController的版本是的修改版本 https://github.com/krisanalfa/lumen-jwt/blob/develop/app/Http/Controllers/Auth/AuthController.php

无论如何,如果您在阅读本书时没有看到在线提供的AuthController,以下是AuthController的内容:

<?php

namespace App\Http\Controllers\Auth;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Tymon\JWTAuth\Facades\JWTAuth;
use App\Http\Controllers\Controller;
use Tymon\JWTAuth\Exceptions\JWTException;
use Illuminate\Http\Exception\HttpResponseException;

class AuthController extends Controller
{
    /**
     * Handle a login request to the application.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return \Illuminate\Http\Response;
     */
    public function login(Request $request)
    {
        try {
            $this->validateLoginRequest($request);
        } catch (HttpResponseException $e) {
            return $this->onBadRequest();
        }

        try {
            // Attempt to verify the credentials and create a token for the user
            if (!$token = JWTAuth::attempt(
                $this->getCredentials($request)
            )) {
                return $this->onUnauthorized();
            }
        } catch (JWTException $e) {
            // Something went wrong whilst attempting to encode the token
            return $this->onJwtGenerationError();
        }

        // All good so return the token
        return $this->onAuthorized($token);
    }

    /**
     * Validate authentication request.
     *
     * @param  Request $request
     * @return void
     * @throws HttpResponseException
     */
    protected function validateLoginRequest(Request $request)
    {
        $this->validate(
            $request, [
                'email' => 'required|email|max:255',
                'password' => 'required',
            ]
        );
    }

    /**
     * What response should be returned on bad request.
     *
     * @return JsonResponse
     */
    protected function onBadRequest()
    {
        return new JsonResponse(
            [
                'message' => 'invalid_credentials'
            ], Response::HTTP_BAD_REQUEST
        );
    }

    /**
     * What response should be returned on invalid credentials.
     *
     * @return JsonResponse
     */
    protected function onUnauthorized()
    {
        return new JsonResponse(
            [
                'message' => 'invalid_credentials'
            ], Response::HTTP_UNAUTHORIZED
        );
    }

    /**
     * What response should be returned on error while generate JWT.
     *
     * @return JsonResponse
     */
    protected function onJwtGenerationError()
    {
        return new JsonResponse(
            [
                'message' => 'could_not_create_token'
            ], Response::HTTP_INTERNAL_SERVER_ERROR
        );
    }

    /**
     * What response should be returned on authorized.
     *
     * @return JsonResponse
     */
    protected function onAuthorized($token)
    {
        return new JsonResponse(
            [
                'message' => 'token_generated',
                'data' => [
                    'token' => $token,
                ]
            ]
        );
    }

    /**
     * Get the needed authorization credentials from the request.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return array
     */
    protected function getCredentials(Request $request)
    {
        return $request->only('email', 'password');
    }

    /**
     * Invalidate a token.
     *
     * @return \Illuminate\Http\Response
     */
    public function invalidateToken()
    {
        $token = JWTAuth::parseToken();

        $token->invalidate();

        return new JsonResponse(['message' => 'token_invalidated']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\Response
     */
    public function refreshToken()
    {
        $token = JWTAuth::parseToken();

        $newToken = $token->refresh();

        return new JsonResponse(
            [
                'message' => 'token_refreshed',
                'data' => [
                    'token' => $newToken
                ]
            ]
        );
    }

    /**
     * Get authenticated user.
     *
     * @return \Illuminate\Http\Response
     */
    public function getUser()
    {
        return new JsonResponse(
            [
                'message' => 'authenticated_user',
                'data' => JWTAuth::parseToken()->authenticate()
            ]
        );
    }
}

此控制器执行三项主要任务:

  • 登录login()方式
  • 无效令牌
  • 刷新令牌

登录

正在使用login()方法进行登录,并尝试使用JWTAuth::attempt($this->getCredentials($request))进行登录。如果凭据无效或存在其他问题,它只会返回一个错误。然而,要访问这个login()方法,我们需要为它添加一个路由。以下是我们将在routes/web.php中添加的内容:

$api->post(
    '/auth/login', [
        'as' => 'api.auth.login',
        'uses' => 'Auth\AuthController@login',
    ]
);

无效令牌

要使令牌失效,即注销用户,将使用invalidateToken()方法。此方法将通过路由调用。我们将使用 delete-request 方法添加以下路由,该方法将从路由文件中调用AuthController::invalidateToken()

$api->delete(
    '/', [
        'uses' => 'Auth/AuthController@invalidateToken',
        'as' => 'api.auth.invalidate'
    ]
);

刷新令牌

当令牌根据到期时间到期时,调用刷新令牌。为了刷新令牌,我们还需要添加以下路由:

$api->patch(
    '/', [
        'uses' => 'Auth/AuthController@refreshToken',
        'as' => 'api.auth.refresh'
    ]
);

请注意,所有这些端点都将添加到 v1 版本下。

一旦我们有了这个AuthController并设置了路由,用户就可以使用以下端点登录:

POST /api/v1/auth/login
Params: email, passsword

试试这个,你会得到一个基于 JWT 的访问令牌。

Lumen, Dingo, JWT Auth, and CORS boilerplate:

If you face difficulty in configuring Lumen with Dingo and JWT, then you can simply use the repository at https://github.com/krisanalfa/lumen-jwt. This repository will provide you with boilerplate code for setting up your Lumen for API development using Dingo API and JWT. You can clone this and simply start using it. It is nothing other than a Lumen integration with JWT, Dingo API, and CORS support. So, if you are starting a new RESTful web services project, you can simply start with this boiler plate code.

在继续之前,让我们查看路由文件,以确保我们位于同一页面上:

<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', [
    'middleware' => ['api.throttle'],
    'limit' => 100,
    'expires' => 5,
    'prefix' => 'api/v1',
    'namespace' => 'App\Http\Controllers\V1'
],
    function ($api) {
 $api->group(['middleware' => 'api.auth'], function ($api) {
            //Posts protected routes
            $api->resource('posts', "PostController", [
                'except' => ['show', 'index']
            ]);

            //Comments protected routes
            $api->resource('comments', "CommentController", [
                'except' => ['show', 'index']
            ]);

            $api->post('posts/{id}/comments', 'CommentController@store');

            // Logout user by removing token
            $api->delete(
                '/', [
                    'uses' => 'Auth/AuthController@invalidateToken',
                    'as' => 'api.Auth.invalidate'
                ]
            );

            // Refresh token
            $api->patch(
                '/', [
                    'uses' => 'Auth/AuthController@refreshToken',
                    'as' => 'api.Auth.refresh'
                ]
            );
 });
 $api->get('posts', 'PostController@index');
 $api->get('posts/{id}', 'PostController@show');

 $api->get('posts/{id}/comments', 'CommentController@index');
 $api->get('comments/{id}', 'CommentController@show');

 $api->post(
 '/auth/login', [
 'as' => 'api.Auth.login',
 'uses' => 'Auth\AuthController@login',
 ]
 );
 });

$app->get('/', function () use ($app) {
 return $app->version();
});

如你所见,我们已经建立了一个路线组。路由组只是对类似路由进行分组的一种方式,我们可以在其中应用相同的中间件、名称空间或前缀等,就像我们在v1组中所做的那样。

在这里,我们制作了另一个路由组,以便在其上添加api.auth中间件。另一件需要注意的事情是,我们已经将一些 post 路由从 post 资源路由拆分为单独的路由,只是为了让一些路由在不登录的情况下可用。我们对评论路线也做了同样的事情。

Note that if you don't want to split some routes from the resource route, then you can do that as well. You will just add the api.auth middleware in controllers instead of routes file. Both ways are correct; it is just a matter of preference. I did it this way because I find it easier to know which routes are protected from the same routes file instead of constructors of different controllers. But again, it is up to you.

我们只允许登录用户创建、更新和删除帖子。但是,我们需要确保登录的用户只能更新或删除自己的帖子。虽然这件事也可以通过创建另一个中间件来完成,但在 controller 中完成会更简单。

我们在PostController中就是这样做的:

<?php

namespace App\Http\Controllers\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Tymon\JWTAuth\Facades\JWTAuth;
use Dingo\Api\Routing\Helpers;

class PostController extends Controller
{
    use Helpers;

    public function __construct(\App\Post $post)
    {

        $this->post = $post;

    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        $posts = $this->post->paginate(20);

        return $posts;
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {

        $input = $request->all();
 $input['user_id'] = $this->user->id;

        $validationRules = [
            'content' => 'required|min:1',
            'title' => 'required|min:1',
            'status' => 'required|in:draft,published',
            'user_id' => 'required|exists:users,id'
        ];

        $validator = \Validator::make($input, $validationRules);
        if ($validator->fails()) {
            return new JsonResponse(
                [
                    'errors' => $validator->errors()
                ], Response::HTTP_BAD_REQUEST
            );
        }

        $this->post->create($input);

        return [
            'data' => $input
        ];
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }

        return $post;
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $input = $request->all();

        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }
 if($this->user->id != $post->user_id){
            return new JsonResponse(
                [
                    'errors' => 'Only Post Owner can update it'
                ], Response::HTTP_FORBIDDEN
            ); }

        $post->fill($input);
        $post->save();

        return $post;
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }
 if($this->user->id != $post->user_id){
            return new JsonResponse(
                [
                    'errors' => 'Only Post Owner can delete it'
                ], Response::HTTP_FORBIDDEN
            ); }

        $post->delete();

        return ['message' => 'deleted successfully', 'post_id' => $id];
    }
}

如您所见,我在三个地方突出显示了代码。在store()方法中,我们从中获取用户 ID 并将其放入输入数组中,这样 post 的user_id将基于令牌。类似地,对于update()delete(),我们使用了该用户的 ID 并进行了检查,以确保帖子所有者正在删除或更新帖子记录。您可能想知道,当我们还没有在任何地方定义$this->user属性时,我们如何访问它?实际上,我们使用的是 Helpers 特征,所以$this->user来自于该特征。

Note that in order to access protected resources, you should grab the token from the login endpoint and put it in your header as follows: Authentication: bearer <token grabbed from login>

同样,CommentController将进行检查,以确保评论修改仅限于评论所有者,删除仅限于评论或帖子所有者。它将以相同的方式通过令牌进行类似的检查和用户 ID。所以,我将把它留给您来实现 CommentController 以进行这些检查。

变形金刚

在全栈 MVC 框架中,我们有模型-视图-控制器。在 Lumen 或 API 中,我们没有视图,因为我们只返回一些数据。然而,我们可能希望以不同于通常的方式显示数据。我们可能想要使用不同的格式,或者想要用特定的格式限制对象。在所有这些情况下,我们都需要一个可以完成格式化相关任务的地方,一个可以拥有不同格式相关内容的地方。我们可以把它放在控制器里。但是,我们需要在不同的地方定义相同的格式。在这种情况下,我们可以在模型中添加一个方法。例如,post 模型可以有一种特定的方式来格式化 post 对象。因此,我们可以在 Post 模型中定义另一种方法。

它可以很好地工作,但如果仔细观察,它与格式有关,而不是与模型有关。因此,我们有另一层称为序列化或转换器。此外,有时我们需要嵌套对象,因此我们不希望一次又一次地进行相同的嵌套。

Lumen 提供了一种将数据序列化为 JSON 的方法。在雄辩对象中,有一个方法名为toJson();可以重写此方法以达到此目的。但是,最好有一个单独的层来格式化和序列化数据,而不是在同一个类中只有一个方法。然后是变形金刚;变压器只是另一层。您可以将转换器视为 API 或 web 服务的视图层。

理解和设置变压器

实际上,我们使用的名为 dingoapi 的包包含了创建 restfulweb 服务所需的大量内容。同样的 Dingo API 包也为变压器提供了支持。

在做任何事情之前,我们需要了解变压器层由变压器组成。transformer 是负责数据表示的类。Dingo API transformers 支持 transformers,对于 transformers,API 依赖于另一个负责 transformer 功能的库。这取决于我们使用哪个转换层或库。默认情况下,它附带了默认的变换层分形。

我们不需要做任何其他与设置相关的事情。让我们开始为我们的对象使用 transformer。然而,在那之前,让自己适应分形。我们至少需要知道分形是什么,它提供了什么。分形的文档可在中找到 http://fractal.thephpleague.com/

使用变压器

有两种方法可以告诉 Lumen 必须使用哪种变压器等级。为此,我们需要创建一个 transformer 类。让我们首先为Post对象制作一个变压器,并将其命名为PostTransformer。首先,创建一个名为app/Transformers的目录,并在该目录中创建一个包含以下内容的类PostTransformer

<?php

namespace App\Transformers;

use League\Fractal;

class PostTransformer extends Fractal\TransformerAbstract
{
    public function transform(\App\Post $post)
    {
        return $post->toArray();
    }
}

您可以在transform()方法中对 Post 响应执行任何您想执行的操作。请注意,这里我们没有选择性地覆盖transform()方法,但我们提供了transform()的一个实现。您始终需要在 transformer 类中添加该方法。但是,如果这个类不是从任何地方使用的,它就没有任何用处。那么,让我们从我们的PostController中使用它。我们用index()方法来做:

public function index(\App\Transformers\PostTransformer $postTransformer)
{
    $posts = $this->post->paginate(20);

 return $this->response->paginator($posts, $postTransformer);
}

如您所见,我们已经将PostTransformer对象注入$this->response->paginator()方法。我们首先需要注意的是$this->response->paginator()方法和$this->response对象。我们现在首先需要知道$this->response物体的来源。我们得到它是因为我们在PostController中使用了Helpers特征。不管怎样,现在,让我们看看它是如何工作的。点击具有以下端点的PostController``index()方法:

http://localhost:8000/api/v1/posts

它将返回如下内容:

{
"data": [
    {
        "id": 1,
        "title": "test",
        "status": "draft",
        "content": "test post",
        "user_id": 2,
        "created_at": null,
        "updated_at": "2017-06-28 00:47:50"
    },
    {
        "id": 3,
        "title": "test",
        "status": "published",
        "content": "test post",
        "user_id": 2,
        "created_at": "2017-06-28 00:00:44",
        "updated_at": "2017-06-28 00:00:44"
        },
    {
        "id": 4,
        "title": "test",
        "status": "published",
        "content": "test post",
        "user_id": 2,
        "created_at": "2017-06-28 03:21:36",
        "updated_at": "2017-06-28 03:21:36"
        },
    {
        "id": 5,
        "title": "test post",
        "status": "draft",
        "content": "This is yet another post for testing purpose",
        "user_id": 8,
        "created_at": "2017-07-15 00:45:29",
        "updated_at": "2017-07-15 00:45:29"
        },
    {
        "id": 6,
        "title": "test post",
        "status": "draft",
        "content": "This is yet another post for testing purpose",
        "user_id": 8,
        "created_at": "2017-07-15 23:53:23",
        "updated_at": "2017-07-15 23:53:23"
        }
],
"meta": {
    "pagination": {
        "total": 5,
            "count": 5,
            "per_page": 20,
            "current_page": 1,
            "total_pages": 1,
            "links": []
        }
    }
}

如果您查看它,您将看到一个单独的元部分,其中包含分页相关的内容。这是分形变换本身提供的一件小事。事实上,分形能为我们做的还有很多。

我们可以包含嵌套对象。例如,如果我们在Post中有user_id,并且我们希望User对象嵌套在同一Post对象中,那么它也可以提供一种更简单的方法。尽管 transformer 层与 API 响应数据的视图层类似,但它提供的远不止这些。现在,我将向您展示我们的PostController方法在使用show()中的PostTransformer和其他方法返回后的效果。有关分形的详细信息,我建议您查看分形文档,以便您可以在上充分利用它 http://fractal.thephpleague.com/

下面是我们的PostController方法的外观:

<?php

namespace App\Http\Controllers\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Dingo\Api\Routing\Helpers;
use App\Transformers\PostTransformer;

class PostController extends Controller
{
 use Helpers;

    public function __construct(\App\Post $post, \App\Transformers\PostTransformer $postTransformer)
    {
        $this->post = $post;

        $this->transformer = $postTransformer;

    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $posts = $this->post->paginate(20);

 return $this->response->paginator($posts, $this->transformer);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {

        $input = $request->all();
        $input['user_id'] = $this->user->id;

        $validationRules = [
            'content' => 'required|min:1',
            'title' => 'required|min:1',
            'status' => 'required|in:draft,published',
            'user_id' => 'required|exists:users,id'
        ];

        $validator = \Validator::make($input, $validationRules);
        if ($validator->fails()) {
            return new JsonResponse(
                [
                    'errors' => $validator->errors()
                ], Response::HTTP_BAD_REQUEST
            );
        }

        $post = $this->post->create($input);

 return $this->response->item($post, $this->transformer);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }

 return $this->response->item($post, $this->transformer);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $input = $request->all();

        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }

        if($this->user->id != $post->user_id){
            return new JsonResponse(
                [
                    'errors' => 'Only Post Owner can update it'
                ], Response::HTTP_FORBIDDEN
            );
        }

        $post->fill($input);
        $post->save();

 return $this->response->item($post, $this->transformer);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $post = $this->post->find($id);

        if(!$post) {
            abort(404);
        }

        if($this->user->id != $post->user_id){
            return new JsonResponse(
                [
                    'errors' => 'Only Post Owner can delete it'
                ], Response::HTTP_FORBIDDEN
            );
        }

        $post->delete();

        return ['message' => 'deleted successfully', 'post_id' => $id];
    }
}

从前面代码片段中突出显示的行中,您可以看到我们在构造函数中添加了PostTransformer对象,并将其放置在我们在其他方法中使用的$this->transformer中。你可以看到的另一件事是,在一个地方,我们在index()方法中使用$this->response->paginator()方法,而在其他地方我们使用$this->response->item()。这是因为$this->response->item()方法是在有一个对象时使用的,而index()方法中有paginator对象时使用的是paginator。请注意,如果您有一个集合而没有paginator对象,则应使用$this->response->collection()

正如前面提到的,分形有更多的特性,这些特性都在它的文档中。因此,您需要暂停一下,并在浏览其文档 http://fractal.thephpleague.com/.

加密

接下来我们缺少的是客户端和服务器之间通信的加密,这样就没有人可以通过网络嗅探和读取数据。为此,我们将使用SSL安全套接字层)。由于这本书不是关于加密或加密或服务器设置的,我们将不深入讨论这些概念的细节,但重要的是我们在这里讨论加密。如果有人能够通过网络嗅探数据,那么我们的网站或 web 服务就不安全。

为了保护我们的 web 服务,我们将使用 HTTPS 而不是 HTTP。HTTPS 中的“S”表示安全。现在的问题是,我们如何才能确保安全。您可能会说,我们将使用 SSL,正如前面所说的那样。那么什么是 SSL?SSL 是安全套接字层,是服务器和浏览器之间安全通信的标准方式。SSL 指的是一种安全协议。实际上 SSL 协议有三个版本,它们对某些攻击是不安全的。所以我们实际使用的是TLS传输层安全。然而,当我们提到 TLS 时,仍然使用 SSL 术语。如果您想使用 SSL 证书和 SSL 使 HTTP 安全,实际上下面使用的是 TLS,它比原始的 SSL 协议更好。

发生的情况是,当建立连接时,服务器将 SSL 证书的副本连同公钥一起发送到浏览器,以便浏览器也可以对与服务器之间的通信进行编码或解码。我们将不讨论加密细节;但是,我们需要知道如何获取 SSL 证书。

SSL 证书,不同的选项

通常,SSL 证书是从证书提供商处购买的。不过,您也可以从letsencrypt.org获得免费证书。那么,如果有免费的证书,为什么还要买证书呢?事实上,有时候,从某些机构购买更多的是保险而不是安全。如果你正在制作一个电子商务网站或接受付款或非常关键的数据(如财务信息),那么你需要有人在网站用户面前承担责任。

来自letsencrypt.org的证书和以高价出售的提供商的证书之间可能有一些细微的区别(我不知道),但通常,购买证书是为了保险,而不是为了安全。

无论您从谁处获得证书,您都将获得安装说明。如果您喜欢使用letsencrypt.org,那么我建议您使用 certbot。按照中的说明操作 https://certbot.eff.org/

总结

在本章中,我们讨论了上一章中我们在 Lumen 中实现 RESTfulWeb 服务端点时缺少的内容。我们讨论了阻止 DoS 或暴力的节流(请求速率限制)。我们还使用一些包实现了基于令牌的身份验证。请注意,这里我们只保护端点,不希望在没有用户登录的情况下保持可访问性。如果您不想对其他端点进行公共访问,但它们不需要用户登录,那么您可以在这些端点上使用某种密钥或基本身份验证。

除此之外,我们还讨论并使用了转换器,它是 web 服务的一种视图层。然后,我们简要讨论了加密和 SSL 的重要性,然后讨论了 SSL 证书的可用选项。

在本章中,我将不向您提供更多资源的列表或 URL,因为我们在本章中讨论了许多不同的内容,因此我们无法深入了解每一个细节。要完全理解它,您应该首先查看我们在这里讨论的每一件事情的文档,然后再进行实践。实际上,当你在练习中遇到问题时,以及当你试图解决问题时,你都会学到。

在下一章中,我们将讨论使用自动化测试工具对端点和代码进行测试和编写测试用例。