PHP應用程序在MVC模式中構建安全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/phinxn./composer.json has been updatednLoading composer repositories with package informationnUpdating dependencies (including require-dev)n[...]nWriting lock filenGenerating autoload filesn
這些軟體包存在著許多其他的依賴關係,有幾個來自於Symfony和Doctrine。不過不要太擔心這些依賴關係。即使他們都與Slim一起安裝, vendor/目錄的大小也只有11MB,這比起任何其他應用程序來說都比較小。
你可能會問,為什麼我們會需要這些程序包?所有這一切難道都不能用簡單的PHP和SQL來完成嗎?這個問題的答案是,這些程序包使得這些功能的開發更快速,因為它們已經經過很好的測試。
「應用程序」結構
現在讓我們開始構建的過程吧,看看我們的應用程序將會是什麼樣子的,我們成功地移動了所有的東西,現在把它分解成各個功能部件。
App/n--> Controller/n--> Model/n--> View/n--> Middleware/nbootstrap/n--> app.phpn--> db.phpn--> routes.phpntemplates/npublic/ndb/n
讓我們一起來看看這個結構。我們的主要命名空間是App應用程序文件。這是App/目錄下的所有文件,包括控制器,模型和任何可能需要的視圖輔助類文件。在bootstrap目錄的內部,我們將為我們的應用程序提供主要的配置文件。包括了一些基本的應用程序設置(如系列文章第一部分中的處理程序)和Slim應用程序配置。資料庫連接信息將存放在db配置文件中,路由設置將在routes配置文件中。
最後的templates目錄,可以存放任何我們可能需要的視圖模板,該db目錄將用於存放Phinx遷移的文件,public是放置了我們的前端控制器index.php文件的目錄。
請注意,我們正在使用一個子目錄作為文檔的根目錄。這有助於防止一些安全問題,例如.env中包含的各種敏感信息的文件可以直接在Web中訪問。
如果你對這些目錄不熟悉,你也不要擔心,在文章的後面,我將帶你操作每一步,並解釋在任何一步中都發生了些什麼。
現在要花點時間進行目錄的創建:
mkdir Appnmkdir bootstrapnmkdir templatesnmkdir publicnmkdir dbn
遷移
現在我們在index.php文件中已經定義了一些代碼:
· 應用程序的引導
· 路由處理
· 根路徑/請求的請求/響應處理程序
構建bootstrap
我們要把已有的代碼進行修改,並把它們分解成我們想要的新結構。首先我們將從bootstrap開始。我們來看一下這個代碼,把它移到一個bootstrap/app.php文件中,看起來像這樣:
<?phpnsession_start();nrequire_once ../vendor/autoload.php;n n$dotenv = new DotenvDotenv(BASE_PATH);n$dotenv->load();n n$app = new SlimApp();n n$container = $app->getContainer();n n// Make the custom App autoloadernspl_autoload_register(function($class) {n $classFile = APP_PATH./../.str_replace(, /, $class)..php;n if (!is_file($classFile)) {n throw new Exception(Cannot load class: .$class);n }n require_once $classFile;n});n n// Autoload in our controllers into the containernforeach (new DirectoryIterator(APP_PATH./Controller) as $fileInfo) {n if($fileInfo->isDot()) continue;n $class = AppController.str_replace(.php, , $fileInfo->getFilename());n $container[$class] = function($c) use ($class){n return new $class();n };n}n n$container[notFoundHandler] = function($container) {n return function ($request, $response) use ($container) {n return $container[response]n ->withStatus(404)n ->withHeader(Content-Type, application/json)n ->write(json_encode([error => Resource not valid]));n };n};n n$container[errorHandler] = function($container) {n return function ($request, $response, $exception = null) use ($container) {n $code = 500;n $message = There was an error;n n if ($exception !== null) {n $code = $exception->getCode();n $message = $exception->getMessage();n }n n // Use this for debugging purposesn /*error_log($exception->getMessage(). in .$exception->getFile(). - (n .$exception->getLine()., .get_class($exception).));*/n n return $container[response]n ->withStatus($code)n ->withHeader(Content-Type, application/json)n ->write(json_encode([n success => false,n error => $messagen ]));n };n};n n$container[notAllowedHandler] = function($container) {n return function ($request, $response) use ($container) {n return $container[response]n ->withStatus(401)n ->withHeader(Content-Type, application/json)n ->write(json_encode([error => Method not allowed]));n };n};n
這是從我們之前創建的代碼中複製粘貼的。在這裡,我們正在創建應用程序,獲取容器並設置我們的自定義處理程序,用於異常和未找到(404)/不允許(405)的問題。但是,文件開始處有一些額外的代碼需要添加。
首先,在我們定義之前,你會注意到SlimApp調用了DotenvDotenv和它的load方法。這個方法會在根目錄中的.env查找要載入的文件。我在系列文章中提到過vlucas/phpdotenv這個包,這就是我們使用它的地方。繼續往下看,在這個項目的根目錄(和public/不是一個級別)中,創建一個名為.env的文件,文件內容如下:
DB_HOST=localhostnDB_NAME=database_namenDB_USER=database_usernDB_PASS=database_passwordn
以上內容為我們提供了我們稍後設置資料庫連接會用到的更新模板。這些值將在運行時通過Dotenv處理程序載入到$_ENV變數中並在整個應用程序中使用。
如果你忘記了設置.env文件或這個文件位於一個錯誤的位置,則該程序包會拋出異常,並且你的應用程序將無法繼續執行。
接下來讓我們來看看自定義自動載入器。由於我們想要在App應用程序的各個部分中引用命名空間中的類,因此我們需要添加一個自定義的自動載入器來處理這些請求。我們利用spl_autoload_register函數來定義這個自動載入器,並使用它的APP_PATH找到匹配的文件。
下面的代碼是Slim在使用控制器時需要的東西。正如我之前提到過的,Slim大量使用依賴注入容器來做很多的事情。這當然也包括了當從路由引用時解析控制器和動作方法。在我們的根路由示例中,我們只是直接輸出了一些東西,但是可以很容易地轉換成如下所示的代碼:
<?phpn nclass IndexControllern{n public function index()n {n echo index!;n }n}n n$app->get(/, IndexController:index);n
上面定義的GET請求路由是Slim用於將HTTP請求正確的路由到IndexController中的index方法。但是,為了實現這一點,我們需要預先載入控制器。DirectoryIterator就是負責預載入的類,它會列出AppController目錄的文件並載入到容器中。這樣就可以輕鬆的定義我們的路由了。
編寫前置控制器
現在我們將把我們的前置控制器放在public/index.php文件中。因為我們需要從我們的引導文件中引入代碼,所以我們將把它包含在文件的起始位置處,並設置一些我們以後可以使用的其他常量:
<?phpndefine(BASE_PATH, __DIR__./..);ndefine(APP_PATH, BASE_PATH./App);n nrequire_once BASE_PATH./vendor/autoload.php;n n// Autorequire everything in BASE_PATH/bootstrap, loading app first - most importantnrequire_once BASE_PATH./bootstrap/app.php;nforeach (new DirectoryIterator(BASE_PATH./bootstrap) as $fileInfo) {n if($fileInfo->isDot()) continue;n require_once $fileInfo->getPathname();n}n n$app->run();n
正如你在上面的代碼中看到的,首先我們定義了可以跨應用程序使用的兩個常量: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自動載入:
<?phpn n$app->get(/, AppControllerIndexController:index);n
為了重新定義默認的/根路由,需要將/請求指向IndexController。由於我們已經將這些控制器注入到了我們的容器中,因此Slim可以解析這個文件並將其發送到需要的地方。我們稍後會在這個控制器中再添加一些功能。現在我們需要設置一個配置文件和資料庫配置。
定義資料庫配置
現在我們將創建資料庫配置,利用Laravels Enloquent包中附帶的「Capsule」功能,就可以在Laravel應用程序之外使用Eloquent功能。由於我們已經使用.env文件定義了我們的資料庫連接信息,所以我們在這裡需要做的是通過一些代碼來設置"Capsule":
<?phpn$dbconfig = [n driver => mysql,n host => $_ENV[DB_HOST],n database => $_ENV[DB_NAME],n username => $_ENV[DB_USER],n password => $_ENV[DB_PASS],n charset => utf8,n collation => utf8_unicode_ci,n prefix => ,n];n n$capsule = new IlluminateDatabaseCapsuleManager;n$capsule->addConnection($dbconfig);n$capsule->setAsGlobal();n$capsule->bootEloquent();n
我在本教程中使用的是MySQL,但也可以使用其他資料庫。請參閱Laravel手冊以確定當前支持哪些資料庫。在上面的代碼中,我們首先從.env文件中定義的$dbconfig數組變數中載入的值來創建資料庫配置。將憑證信息保存在環境變數中可以防止敏感信息泄露。
最後,我們通過$capsule對象的addConnection方法創建並傳遞資料庫配置。最後兩行代碼能夠使我們在全局應用程序中無縫地使用Eloquent的功能。
把代碼放在一起
我們正在進入這個系列最為重要的部分。由於我們之前已經把一些重要的事情準備好了,所以把這些功能合併起來就比較容易了。
我們先從「base」控制器開始,這個控制器包含了一些簡單的方法,然後我們可以在所有的控制器中調用。一些OOP / MVC的純粹主義者可能會不贊同這個想法。創建一個新的文件AppControllerBaseController.php包含如下代碼:
<?phpnnamespace AppController;n nclass BaseControllern{n protected $container;n n /**n * Initialize the controller with the containern *n * @param SlimContainer $container Container instancen */n public function __construct(SlimContainer $container)n {n $this->container = $container;n }n n /**n * Magic method to get things off of the container by referencingn * them as properties on the current objectn */n public function __get($property)n {n // Special property fetch for usern if ($property == user) {n return $user = $this->container->get(session)->get(user);n }n n if (isset($this->container, $property)) {n return $this->container->$property;n }n return null;n }n n /**n * Handle the response and put it into a standard JSON structuren *n * @param boolean $status Pass/fail status of the requestn * @param string $message Message to put in the response [optional]n * @param array $addl Set of additional information to add to the response [optional]n */n public function jsonResponse($status, $message = null, array $addl = [])n {n $output = [success => $status];n if ($message !== null) {n $output[message] = $message;n }n if (!empty($addl)) {n $output = array_merge($output, $addl);n }n n $response = $this->response->withHeader(Content-type, application/json);n $body = $response->getBody();n $body->write(json_encode($output));n n return $response;n }n n /**n * Handle a failure responsen *n * @param string $message Message to put in response [optional]n * @param array $addl Set of additional information to add to the response [optional]n */n public function jsonFail($message = null, array $addl = [])n {n return $this->jsonResponse(false, $message, $addl);n }n n /**n * Handle a success responsen *n * @param string $message Message to put in response [optional]n * @param array $addl Set of additional information to add to the response [optional]n */n public function jsonSuccess($message = null, array $addl = [])n {n return $this->jsonResponse(true, $message, $addl);n }n}n
我們的BaseController只是定義了一些輔助方法,例如JSON響應的輸出標準化。jsonSuccess和jsonFail只是jsonResponse方法的抽象方法。
另外還定義了__get方法。這是一種PHP魔術方法,當從不存在或不是公開的對象請求屬性時將調用此方法。在這種情況下,我們希望能夠從容器中獲得更多的東西。此外,它還有一些額外的代碼,例如讓用戶註銷會話等。
此外,你還將注意到,我們正在使用BaseController的__construct方法接收當前容器的初始化實例。Slim在調用控制器時自動執行此操作,這使得基本控制器和擴展它的類都可以訪問到該控制器。
接下來,我們將創建IndexController來處理/請求,所以AppControllerIndexController.php文件的代碼如下:
<?phpnnamespace AppController;n nclass IndexController extends AppControllerBaseControllern{n public function index()n {n return $this->jsonSuccess(Hello world!);n }n}n
你會注意到我們已經利用jsonSuccess方法返回了一個 「Hello world!」 。
發起請求
現在,一切都已準備就緒,你可以通過簡單的HTTP調用來測試調用API的結果。首先,我們使用之前用過的PHP內置的Web伺服器來啟動應用程序:
cd public/nphp -S localhost:8000n
現在你可以在瀏覽器中訪問此地址:http://localhost:8000。如果一切順利的話,你應該可以看到如下響應:
{n success: true,n message: "Hello world!"n}n
或者,你也可以使用curl來發起請求:
$ curl http://localhost:8000n{"success":true,"message":"Hello world!"}n
寫在最後
在這一部分中我做了很多代碼重構的事情,並為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 更多內容請關注「嘶吼專業版」——Pro4hou
推薦閱讀:
※一個失敗的合作!導致超過1.4億的Verizon用戶數據泄露
※繞過DKIM驗證,偽造釣魚郵件
※Web 網頁爬蟲對抗指南 Part.2
※從明年開始 FBI可任意控制你的電腦
※圖解IP協議(一) IP協議原理與實踐
TAG:信息安全 |