PHP应用程序在MVC模式中构建安全API

Posted 嘶吼专业版

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP应用程序在MVC模式中构建安全API相关的知识,希望对你有一定的参考价值。

继续工作

在本系列文章的第一部分和第二部分我介绍了一些我们构建API所需要的基础库和基本概念。现在我们将进入本系列文章的第三部分,在这之前,我想再回顾一下第一和第二部分的内容,总结一些可以帮助我们走的更长远的一些东西。我相信你已经注意到(在这个Git 仓库中查看本系列文章的“第二部分”的分支上的代码)在我们的index.php文件中的代码量有点大。我们已经定义了主应用程序,并为自定义处理程序更改了一些配置选项。即便只是简单的使用这些代码,保存到一个文件里也会变得有点冗长。(以上部分如需查阅可在原文中查阅)

PHP应用程序在MVC模式中构建安全API 使用MVC设计模式

在这个系列的文章中我们实现了很多功能,你可以将这些功能全部保存到一个文件中,不过,这将成为日后进行代码维护的“恶梦”。为了帮助我们解决便于代码维护的问题,我将使用一个用来处理大型应用程序的方法:模型/视图/控制器设计模式。

如果你还不熟悉这种结构,请看下面的简单介绍:

· 模型表示要处理的数据。在大多数数据库驱动的应用程序中,它们将与表直接关联,每个实体类型之间都存在着关系。

· 视图表示应用程序的输出,即客户端的html,在我们本系列文章中的API的输出是JSON或XML。

· 控制器是将模型和视图绑定在一起的“粘合剂”,并在将值发送到视图进行输出之前对值进行一些额外的处理。

这种结构的目标是基于单一责任原则将应用程序的功能分解成块。应用程序中的每个类/对象只能做一件事情。还有其他的部分被包含在功能更强大的MVC框架中,如服务提供者和其他业务逻辑处理程序,但我们在这里会使用简单的几个功能。虽然我们现在做的事情会涉及到一些额外的处理,但总的来说,我们将坚持使用纯粹的MVC组件。

我们将通过一些中间件功能扩展这个MVC结构,这一点我们在第一部分中简要的介绍过,让我们创建可重复使用的单用途的功能模块,这样我们就可以在整个系统中重复使用。

PHP应用程序在MVC模式中构建安全API 从我的朋友那得到的一点帮助

在PHP生态系统中有大量的MVC框架,我们可能会使用其中任何一个来完成我们在这里做的大部分工作。

正如你已经看到的那样,Slim框架为我们的应用程序提供了最主要的“骨架”,使我们能够将URL中的请求路由到正确的功能上。正如它的名字一样,这就是它所带来的所有功能。还有其他一些我们会用到的功能,主要是请求和响应处理。

vlucas/phpdotenv

该库用于从.env文件读取定义的内容(默认为当前目录)。这些.env文件包含你的应用程序的设置,并且可以将应用程序的设置保留在代码之外。然后将它们加载到$_ENV变量中,以便在应用程序中的任何地方都可以轻松引用。

aura/session

默认情况下,Slim是不附带会话处理程序的,使用PHP自己的$_SESSION功能可能会有点混乱。相反,我已经选择使用Aura组件集合中的这个包来帮助会话功能保持简洁。它在$_SESSION内部使用处理程序,所以它仍然使用相同的功能,只是会提供一个友好的界面。

illuminate/database

这是Laravel框架中的数据库组件,这个组件能使数据库表中的数据变得更简单。它是一个ORM(对象关系映射器)工具,它使用ActiveRecord结构来引用数据库中表示的实体和集合。该软件包还包括了我们将用于设置我们的连接的功能——Capsule。

doctrine/dbal

这个库需要使用Laravel数据库组件进行一些手动数据库查询。虽然从一开始可能不需要这个组件,但如果需要更复杂的查询,那么它将会派上用场。

robmorgan/phinx

最后,我们将安装Phinx数据库迁移管理器。这个Illuminate/database包在创建表之后需要处理表的所有事情,但我们仍然需要创建它们。Phinx可以轻松的根据需要运行或回滚迁移,并且比使用一大堆原始SQL语句更不容易出错。

要全部安装以上这些组件,可以执行下面这条简单的命令:

> composer require vlucas/phpdotenv aura/session illuminate/database doctrine/dbal robmorgan/phinx

./composer.json has been updated

Loading composer repositories with package information

Updating dependencies (including require-dev)

[...]

Writing lock file

Generating autoload files

这些软件包存在着许多其他的依赖关系,有几个来自于Symfony和Doctrine。不过不要太担心这些依赖关系。即使他们都与Slim一起安装, vendor/目录的大小也只有11MB,这比起任何其他应用程序来说都比较小。

你可能会问,为什么我们会需要这些程序包?所有这一切难道都不能用简单的PHP和SQL来完成吗?这个问题的答案是,这些程序包使得这些功能的开发更快速,因为它们已经经过很好的测试。

PHP应用程序在MVC模式中构建安全API “应用程序”结构

现在让我们开始构建的过程吧,看看我们的应用程序将会是什么样子的,我们成功地移动了所有的东西,现在把它分解成各个功能部件。

App/

--> Controller/

--> Model/

--> View/

--> Middleware/

bootstrap/

--> app.php

--> db.php

--> routes.php

templates/

public/

db/

让我们一起来看看这个结构。我们的主要命名空间是App应用程序文件。这是App/目录下的所有文件,包括控制器,模型和任何可能需要的视图辅助类文件。在bootstrap目录的内部,我们将为我们的应用程序提供主要的配置文件。包括了一些基本的应用程序设置(如系列文章第一部分中的处理程序)和Slim应用程序配置。数据库连接信息将存放在db配置文件中,路由设置将在routes配置文件中。

最后的'templates'目录,可以存放任何我们可能需要的视图模板,该db目录将用于存放Phinx迁移的文件,public是放置了我们的前端控制器index.php文件的目录。

请注意,我们正在使用一个子目录作为文档的根目录。这有助于防止一些安全问题,例如.env中包含的各种敏感信息的文件可以直接在Web中访问。

如果你对这些目录不熟悉,你也不要担心,在文章的后面,我将带你操作每一步,并解释在任何一步中都发生了些什么。

现在要花点时间进行目录的创建:

mkdir App

mkdir bootstrap

mkdir templates

mkdir public

mkdir db

PHP应用程序在MVC模式中构建安全API 迁移

现在我们在index.php文件中已经定义了一些代码:

· 应用程序的引导

· 路由处理

· 根路径/请求的请求/响应处理程序

PHP应用程序在MVC模式中构建安全API 构建bootstrap

我们要把已有的代码进行修改,并把它们分解成我们想要的新结构。首先我们将从bootstrap开始。我们来看一下这个代码,把它移到一个bootstrap/app.php文件中,看起来像这样:

<?php

session_start();

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

 

$dotenv = new DotenvDotenv(BASE_PATH);

$dotenv->load();

 

$app = new SlimApp();

 

$container = $app->getContainer();

 

// Make the custom App autoloader

spl_autoload_register(function($class) {

    $classFile = APP_PATH.'/../'.str_replace('', '/', $class).'.php';

    if (!is_file($classFile)) {

        throw new Exception('Cannot load class: '.$class);

    }

    require_once $classFile;

});

 

// Autoload in our controllers into the container

foreach (new DirectoryIterator(APP_PATH.'/Controller') as $fileInfo) {

    if($fileInfo->isDot()) continue;

    $class = 'AppController'.str_replace('.php', '', $fileInfo->getFilename());

    $container[$class] = function($c) use ($class){

        return new $class();

    };

}

 

$container['notFoundHandler'] = function($container) {

    return function ($request, $response) use ($container) {

        return $container['response']

            ->withStatus(404)

            ->withHeader('Content-Type', 'application/json')

            ->write(json_encode(['error' => 'Resource not valid']));

    };

};

 

$container['errorHandler'] = function($container) {

    return function ($request, $response, $exception = null) use ($container) {

        $code = 500;

        $message = 'There was an error';

 

        if ($exception !== null) {

            $code = $exception->getCode();

            $message = $exception->getMessage();

        }

 

        // Use this for debugging purposes

        /*error_log($exception->getMessage().' in '.$exception->getFile().' - ('

            .$exception->getLine().', '.get_class($exception).')');*/

 

        return $container['response']

            ->withStatus($code)

            ->withHeader('Content-Type', 'application/json')

            ->write(json_encode([

                'success' => false,

                'error' => $message

            ]));

    };

};

 

$container['notAllowedHandler'] = function($container) {

    return function ($request, $response) use ($container) {

        return $container['response']

            ->withStatus(401)

            ->withHeader('Content-Type', 'application/json')

            ->write(json_encode(['error' => 'Method not allowed']));

    };

};


这是从我们之前创建的代码中复制粘贴的。在这里,我们正在创建应用程序,获取容器并设置我们的自定义处理程序,用于异常和未找到(404)/不允许(405)的问题。但是,文件开始处有一些额外的代码需要添加。

首先,在我们定义之前,你会注意到SlimApp调用了DotenvDotenv和它的load方法。这个方法会在根目录中的.env查找要加载的文件。我在系列文章中提到过vlucas/phpdotenv这个包,这就是我们使用它的地方。继续往下看,在这个项目的根目录(和public/不是一个级别)中,创建一个名为.env的文件,文件内容如下:

DB_HOST=localhost

DB_NAME=database_name

DB_USER=database_user

DB_PASS=database_password

以上内容为我们提供了我们稍后设置数据库连接会用到的更新模板。这些值将在运行时通过Dotenv处理程序加载到$_ENV变量中并在整个应用程序中使用。

如果你忘记了设置.env文件或这个文件位于一个错误的位置,则该程序包会抛出异常,并且你的应用程序将无法继续执行。

接下来让我们来看看自定义自动加载器。由于我们想要在App应用程序的各个部分中引用命名空间中的类,因此我们需要添加一个自定义的自动加载器来处理这些请求。我们利用spl_autoload_register函数来定义这个自动加载器,并使用它的APP_PATH找到匹配的文件。

下面的代码是Slim在使用控制器时需要的东西。正如我之前提到过的,Slim大量使用依赖注入容器来做很多的事情。这当然也包括了当从路由引用时解析控制器和动作方法。在我们的根路由示例中,我们只是直接输出了一些东西,但是可以很容易地转换成如下所示的代码:

<?php

class IndexController

{

    public function index()

    {

        echo 'index!';

    }


$app->get('/', 'IndexController:index');

上面定义的GET请求路由是Slim用于将HTTP请求正确的路由到IndexController中的index方法。但是,为了实现这一点,我们需要预先加载控制器。DirectoryIterator就是负责预加载的类,它会列出AppController目录的文件并加载到容器中。这样就可以轻松的定义我们的路由了。

编写前置控制器

现在我们将把我们的前置控制器放在public/index.php文件中。因为我们需要从我们的引导文件中引入代码,所以我们将把它包含在文件的起始位置处,并设置一些我们以后可以使用的其他常量:

<?php

define('BASE_PATH', __DIR__.'/..');

define('APP_PATH', BASE_PATH.'/App');

 

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

 

// Autorequire everything in BASE_PATH/bootstrap, loading app first - most important

require_once BASE_PATH.'/bootstrap/app.php';

foreach (new DirectoryIterator(BASE_PATH.'/bootstrap') as $fileInfo) {

    if($fileInfo->isDot()) continue;

    require_once $fileInfo->getPathname();

}

 

$app->run();


正如你在上面的代码中看到的,首先我们定义了可以跨应用程序使用的两个常量:BASE_PATH定义了Web应用程序的根目录(和public/是一个级别的), APP_PATH指向根目录下的App/文件夹。下面我们需要使用Composer将 BASE_PATH指向的路径作为源进行自动加载。

再往下一点的代码块会首先加载我们先前创建的引导文件bootstrap/app.php,这个文件定义了应用程序和处理程序。然后,使用DirectoryIterator加载bootstrap/目录中的任何文件。这样我们会在后面就能够更容易的添加更多的配置设置,包括我们的数据库和路由配置,而无需将它们手动包含在引导文件中。

public/index.php示例文件中的最后一行代码是调用应用程序对象上的run方法。这个方法是告诉Slim应该处理传入请求并输出响应(请求生命周期)的方法。

设置请求路由

现在我们已经编写了引导代码和前置控制器,我们需要使用新的MVC结构重新定义默认的/根路由。在bootstrap/目录中创建一个新文件:bootstrap/routes.php。这个文件由我们的bootstrap/app.php自动加载:

<?php


$app->get('/', 'AppControllerIndexController:index');

为了重新定义默认的/根路由,需要将/请求指向IndexController。由于我们已经将这些控制器注入到了我们的容器中,因此Slim可以解析这个文件并将其发送到需要的地方。我们稍后会在这个控制器中再添加一些功能。现在我们需要设置一个配置文件和数据库配置。

定义数据库配置

现在我们将创建数据库配置,利用Laravel's Enloquent包中附带的“Capsule”功能,就可以在Laravel应用程序之外使用Eloquent功能。由于我们已经使用.env文件定义了我们的数据库连接信息,所以我们在这里需要做的是通过一些代码来设置"Capsule":

<?php

$dbconfig = [

    'driver'    => 'mysql',

    'host'      => $_ENV['DB_HOST'],

    'database'  => $_ENV['DB_NAME'],

    'username'  => $_ENV['DB_USER'],

    'password'  => $_ENV['DB_PASS'],

    'charset'   => 'utf8',

    'collation' => 'utf8_unicode_ci',

    'prefix'    => '',

];

 

$capsule = new IlluminateDatabaseCapsuleManager;

$capsule->addConnection($dbconfig);

$capsule->setAsGlobal();

$capsule->bootEloquent();


我在本教程中使用的是MySQL,但也可以使用其他数据库。请参阅Laravel手册以确定当前支持哪些数据库。在上面的代码中,我们首先从.env文件中定义的$dbconfig数组变量中加载的值来创建数据库配置。将凭证信息保存在环境变量中可以防止敏感信息泄露。

最后,我们通过$capsule对象的addConnection方法创建并传递数据库配置。最后两行代码能够使我们在全局应用程序中无缝地使用Eloquent的功能。

PHP应用程序在MVC模式中构建安全API 把代码放在一起

我们正在进入这个系列最为重要的部分。由于我们之前已经把一些重要的事情准备好了,所以把这些功能合并起来就比较容易了。

我们先从“base”控制器开始,这个控制器包含了一些简单的方法,然后我们可以在所有的控制器中调用。一些OOP / MVC的纯粹主义者可能会不赞同这个想法。创建一个新的文件AppControllerBaseController.php包含如下代码:

<?php

namespace AppController;

 

class BaseController

{

    protected $container;

 

    /**

     * Initialize the controller with the container

     *

     * @param SlimContainer $container Container instance

     */

    public function __construct(SlimContainer $container)

    {

        $this->container = $container;

    }

 

    /**

     * Magic method to get things off of the container by referencing

     * them as properties on the current object

     */

    public function __get($property)

    {

        // Special property fetch for user

        if ($property == 'user') {

            return $user = $this->container->get('session')->get('user');

        }

 

        if (isset($this->container, $property)) {

            return $this->container->$property;

        }

        return null;

    }

 

    /**

     * Handle the response and put it into a standard JSON structure

     *

     * @param boolean $status Pass/fail status of the request

     * @param string $message Message to put in the response [optional]

     * @param array $addl Set of additional information to add to the response [optional]

     */

    public function jsonResponse($status, $message = null, array $addl = [])

    {

        $output = ['success' => $status];

        if ($message !== null) {

            $output['message'] = $message;

        }

        if (!empty($addl)) {

            $output = array_merge($output, $addl);

        }

 

        $response = $this->response->withHeader('Content-type', 'application/json');

        $body = $response->getBody();

        $body->write(json_encode($output));

 

        return $response;

    }

 

    /**

     * Handle a failure response

     *

     * @param string $message Message to put in response [optional]

     * @param array $addl Set of additional information to add to the response [optional]

     */

    public function jsonFail($message = null, array $addl = [])

    {

        return $this->jsonResponse(false, $message, $addl);

    }

 

    /**

     * Handle a success response

     *

     * @param string $message Message to put in response [optional]

     * @param array $addl Set of additional information to add to the response [optional]

     */

    public function jsonSuccess($message = null, array $addl = [])

    {

        return $this->jsonResponse(true, $message, $addl);

    }

}


我们的BaseController只是定义了一些辅助方法,例如JSON响应的输出标准化。jsonSuccess和jsonFail只是jsonResponse方法的抽象方法。

另外还定义了__get方法。这是一种PHP魔术方法,当从不存在或不是公开的对象请求属性时将调用此方法。在这种情况下,我们希望能够从容器中获得更多的东西。此外,它还有一些额外的代码,例如让用户注销会话等。

此外,你还将注意到,我们正在使用BaseController的__construct方法接收当前容器的初始化实例。Slim在调用控制器时自动执行此操作,这使得基本控制器和扩展它的类都可以访问到该控制器。

接下来,我们将创建IndexController来处理/请求,所以AppControllerIndexController.php文件的代码如下:

<?php

namespace AppController;

 

class IndexController extends AppControllerBaseController

{

    public function index()

    {

        return $this->jsonSuccess('Hello world!');

    }

}


你会注意到我们已经利用jsonSuccess方法返回了一个 “Hello world!” 。

PHP应用程序在MVC模式中构建安全API 发起请求

现在,一切都已准备就绪,你可以通过简单的HTTP调用来测试调用API的结果。首先,我们使用之前用过的PHP内置的Web服务器来启动应用程序:

cd public/

php -S localhost:8000

{

   success: true,

    message: "Hello world!"

}

或者,你也可以使用curl来发起请求:

$ curl http://localhost:8000

{"success":true,"message":"Hello world!"}

PHP应用程序在MVC模式中构建安全API 写在最后

在这一部分中我做了很多代码重构的事情,并为API应用程序增加了复杂性。我知道创建一个“简单的”API似乎有点不太可能,但是请相信我,当我们添加其他功能时,你就会觉得更容易了。

和之前一样,你可以通过查看GitHub仓库,获取我们创建的最新版本的API代码: https://github.com/psecio/secure-api。master分支是最新的版本,每个“part *”分支是该系列中每一部分的代码。如果你在本地创建的代码中出现了错误,请在仓库中找到正确的代码,看看它们之间是否存在差异。

最后,我们需要回顾一下,这个系列的第三部分所做的大部分事情都是在重构应用程序,目的是使得在未来的构建工作中能简单地整合多个API,为今后的事情奠定基础。通过这种重构,我们可以开始了解一些有趣的事情:用户登录的设计以及使用某些中间件来让工作变得更加简单。

PHP应用程序在MVC模式中构建安全API 资源

请点击下方“阅读原文”查看。

以上是关于PHP应用程序在MVC模式中构建安全API的主要内容,如果未能解决你的问题,请参考以下文章

将 Angular 和 .net core web api 安全地集成到现有的 MVC 5 应用程序中

开发自己的PHP MVC框架

php mvc框架的理解

Core MVC

如何构建 Javascript 架构以补充 PHP MVC Web 应用程序?

如何使用PHP / MySQL实现良好的MVC模式而不会丢失SQL请求时间/服务器内存? (良好做法)