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

丝绸之路 Web安全 2017年11月20日发布
Favorite收藏

导语:继续工作 在本系列文章的第一部分和第二部分我介绍了一些我们构建API所需要的基础库和基本概念。现在我们将进入本系列文章的第三部分,在这之前,我想再回顾一下第一和第二部分的内容,总结一些可以帮助我们走的更长远的一些东西。我相信

继续工作

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

使用MVC设计模式

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

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

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

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

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

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

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

从我的朋友那得到的一点帮助

在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来完成吗?这个问题的答案是,这些程序包使得这些功能的开发更快速,因为它们已经经过很好的测试。

“应用程序”结构

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

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

迁移

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

· 应用程序的引导

· 路由处理

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

构建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的功能。

把代码放在一起

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

我们先从“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!” 。

发起请求

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

cd public/
php -S localhost:8000

现在你可以在浏览器中访问此地址:http://localhost:8000。如果一切顺利的话,你应该可以看到如下响应:

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

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

$ curl http://localhost:8000
{"success":true,"message":"Hello world!"}

写在最后

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

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

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

资源

https://github.com/psecio/secure-api

第1部分

第2部分

本文翻译自:https://websec.io/2017/05/12/Build-Secure-API-Part3.html ,如若转载,请注明原文地址: http://www.4hou.com/technology/8530.html
点赞 4
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论