用 PHP 构建简单的 REST API





0/5 (0投票)
在本文中,我将向您展示如何从头开始使用 PHP 构建一个简单的 REST API。我们将使用 Okta 作为授权提供商并实现客户端凭据流来确保 API 的安全性。
REST API 是现代 Web 开发的支柱。如今,大多数 Web 应用程序都开发为前端的单页应用程序,并连接到用各种语言编写的后端 API。有许多出色的框架可以帮助您快速构建 REST API。在 PHP 生态系统中,Laravel/Lumen 和 Symfony 的 API 平台是最常用的示例。它们提供了强大的工具来处理请求并生成具有正确 HTTP 状态码的 JSON 响应。它们还使处理身份验证/授权、请求验证、数据转换、分页、过滤器、速率限制、包含子资源的复杂端点以及 API 文档等常见问题变得容易。
当然,您不需要一个复杂的框架来构建一个简单但安全的 API。在本文中,我将向您展示如何从头开始使用 PHP 构建一个简单的 REST API。我们将使用 Okta 作为授权提供商并实现客户端凭据流来确保 API 的安全性。
Okta 是一个 API 服务,允许您创建、编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接起来。 注册一个永久免费的开发者帐户,完成后,请返回继续学习如何使用 PHP 构建简单的 REST API。
OAuth 2.0 有不同的身份验证流,具体取决于客户端应用程序是公开的还是私有的,以及是否涉及用户,或者通信是否仅限于机器对机器。客户端凭据流最适合机器对机器通信,其中客户端应用程序是私有的(并且可以信任它来保管密钥)。在文章的最后,我将向您展示如何构建一个测试客户端应用程序。
为您的 REST API 创建 PHP 项目骨架
我们将首先创建一个 /src 目录,并在顶层目录中创建一个简单的 composer.json 文件,该文件(目前)只有一个依赖项:DotEnv 库,它将允许我们将 Okta 身份验证详细信息保存在 .env 文件中,而不是保存在代码库中。
composer.json
{ "require": { "vlucas/phpdotenv": "^2.4" }, "autoload": { "psr-4": { "Src\\": "src/" } }}
我们还配置了一个 PSR-4 自动加载器,它将在 /src
目录中自动查找 PHP 类。
我们现在可以安装我们的依赖项了
composer install
现在我们有了一个 /vendor 目录,并且 DotEnv 依赖项已安装(我们还可以使用我们的自动加载器从 /src 加载类,而无需 include()
调用)。
让我们为我们的项目创建一个 .gitignore 文件,其中包含两行,这样 /vendor 目录和我们的本地 .env 文件将被忽略。
vendor/ .env
接下来,我们将为我们的 Okta 身份验证变量创建一个 .env.example 文件。
.env.example
OKTAAUDIENCE=api://default OKTAISSUER= SCOPE= OKTACLIENTID= OKTASECRET=
以及一个 .env 文件,我们稍后将在其中填入我们 Okta 帐户的实际详细信息(它将被 Git 忽略,因此不会出现在我们的存储库中)。
我们需要一个 bootstrap.php 文件来加载我们的环境变量(稍后它还将为我们的项目进行一些额外的引导)。
bootstrap.php
<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = new DotEnv(__DIR__);
$dotenv->load();
// test code, should output:
// api://default
// when you run $ php bootstrap.php
echo getenv('OKTAAUDIENCE');
为您的 PHP REST API 配置数据库
我们将使用 MySQL 为我们的简单 API 提供支持。我们将为我们的应用程序创建一个新的数据库和用户。
mysql -uroot -p
CREATE DATABASE api_example CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'api_user'@'localhost' identified by 'api_password';
GRANT ALL on api_example.* to 'api_user'@'localhost';
quit
我们的 REST API 将只处理一个实体:Person,具有以下字段:id
、firstname
、lastname
、firstparent_id
、secondparent_id
。它将允许我们定义人员以及每个人的最多两个父母(链接到我们数据库中的其他记录)。让我们在 MySQL 中创建数据库表。
mysql -uapi_user -papi_password api_example
CREATE TABLE person (
id INT NOT NULL AUTO_INCREMENT,
firstname VARCHAR(100) NOT NULL,
lastname VARCHAR(100) NOT NULL,
firstparent_id INT DEFAULT NULL,
secondparent_id INT DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (firstparent_id)
REFERENCES person(id)
ON DELETE SET NULL,
FOREIGN KEY (secondparent_id)
REFERENCES person(id)
ON DELETE SET NULL
) ENGINE=INNODB;
我们将把数据库连接变量添加到我们的 .env.example 文件中。
.env.example
DB_HOST=localhost DB_PORT=3306 DB_DATABASE= DB_USERNAME= DB_PASSWORD=
然后我们将把我们的本地凭据输入到 .env 文件中(请记住,它不会存储在存储库中)。
.env
DB_HOST=localhost DB_PORT=3306 DB_DATABASE=api_example DB_USERNAME=api_user DB_PASSWORD=api_password
我们现在可以创建一个类来保存我们的数据库连接,并将连接的初始化添加到我们的 bootstrap.php 文件中。
src/System/DatabaseConnector.php
<?php
namespace Src\System;
class DatabaseConnector {
private $dbConnection = null;
public function __construct()
{
$host = getenv('DB_HOST');
$port = getenv('DB_PORT');
$db = getenv('DB_DATABASE');
$user = getenv('DB_USERNAME');
$pass = getenv('DB_PASSWORD');
try {
$this->dbConnection = new \PDO(
"mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
$user,
$pass
);
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function getConnection()
{
return $this->dbConnection;
}
}
bootstrap.php (完整版)
<?php
require 'vendor/autoload.php';
use Dotenv\Dotenv;
use Src\System\DatabaseConnector;
$dotenv = new DotEnv(__DIR__);
$dotenv->load();
$dbConnection = (new DatabaseConnector())->getConnection();
让我们创建一个 dbseed.php 文件,它将创建我们的 Person
表并在其中插入一些记录以供测试。
dbseed.php
<?php
require 'bootstrap.php';
$statement = <<<EOS
CREATE TABLE IF NOT EXISTS person (
id INT NOT NULL AUTO_INCREMENT,
firstname VARCHAR(100) NOT NULL,
lastname VARCHAR(100) NOT NULL,
firstparent_id INT DEFAULT NULL,
secondparent_id INT DEFAULT NULL,
PRIMARY KEY (id),
FOREIGN KEY (firstparent_id)
REFERENCES person(id)
ON DELETE SET NULL,
FOREIGN KEY (secondparent_id)
REFERENCES person(id)
ON DELETE SET NULL
) ENGINE=INNODB;
INSERT INTO person
(id, firstname, lastname, firstparent_id, secondparent_id)
VALUES
(1, 'Krasimir', 'Hristozov', null, null),
(2, 'Maria', 'Hristozova', null, null),
(3, 'Masha', 'Hristozova', 1, 2),
(4, 'Jane', 'Smith', null, null),
(5, 'John', 'Smith', null, null),
(6, 'Richard', 'Smith', 4, 5),
(7, 'Donna', 'Smith', 4, 5),
(8, 'Josh', 'Harrelson', null, null),
(9, 'Anna', 'Harrelson', 7, 8);
EOS;
try {
$createTable = $dbConnection->exec($statement);
echo "Success!\n";
} catch (\PDOException $e) {
exit($e->getMessage());
}
我们的数据库已设置完毕!如果您想重置它,只需在 MySQL 中删除 person
表,然后运行 php dbseed.php
(我没有将 drop 语句添加到 seeder 中,以防万一误操作)。
为 Person 表添加一个 Gateway 类
在面向对象的环境中,有许多与数据库交互的模式,从按需执行直接 SQL 语句(以过程化方式)到复杂的 ORM 系统(PHP 中最流行的 ORM 选择是 Eloquent 和 Doctrine)。对于我们的简单 API,使用简单的模式也是有意义的,因此我们将采用 Table Gateway。我们将跳过创建 Person
类(按照经典模式的要求),而只使用 PersonGateway
类。我们将实现返回所有记录、返回特定人员以及添加/更新/删除人员的方法。
src/TableGateways/PersonGateway.php
<?php
namespace Src\TableGateways;
class PersonGateway {
private $db = null;
public function __construct($db)
{
$this->db = $db;
}
public function findAll()
{
$statement = "
SELECT
id, firstname, lastname, firstparent_id, secondparent_id
FROM
person;
";
try {
$statement = $this->db->query($statement);
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
return $result;
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function find($id)
{
$statement = "
SELECT
id, firstname, lastname, firstparent_id, secondparent_id
FROM
person
WHERE id = ?;
";
try {
$statement = $this->db->prepare($statement);
$statement->execute(array($id));
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
return $result;
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function insert(Array $input)
{
$statement = "
INSERT INTO person
(firstname, lastname, firstparent_id, secondparent_id)
VALUES
(:firstname, :lastname, :firstparent_id, :secondparent_id);
";
try {
$statement = $this->db->prepare($statement);
$statement->execute(array(
'firstname' => $input['firstname'],
'lastname' => $input['lastname'],
'firstparent_id' => $input['firstparent_id'] ?? null,
'secondparent_id' => $input['secondparent_id'] ?? null,
));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function update($id, Array $input)
{
$statement = "
UPDATE person
SET
firstname = :firstname,
lastname = :lastname,
firstparent_id = :firstparent_id,
secondparent_id = :secondparent_id
WHERE id = :id;
";
try {
$statement = $this->db->prepare($statement);
$statement->execute(array(
'id' => (int) $id,
'firstname' => $input['firstname'],
'lastname' => $input['lastname'],
'firstparent_id' => $input['firstparent_id'] ?? null,
'secondparent_id' => $input['secondparent_id'] ?? null,
));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
public function delete($id)
{
$statement = "
DELETE FROM person
WHERE id = :id;
";
try {
$statement = $this->db->prepare($statement);
$statement->execute(array('id' => $id));
return $statement->rowCount();
} catch (\PDOException $e) {
exit($e->getMessage());
}
}
}
显然,在生产系统中,您会希望更优雅地处理异常,而不是仅仅以错误消息退出。
以下是一些使用 gateway 的示例。
$personGateway = new PersonGateway($dbConnection);
// return all records
$result = $personGateway->findAll();
// return the record with id = 1
$result = $personGateway->find(1);
// insert a new record
$result = $personGateway->insert([
'firstname' => 'Doug',
'lastname' => 'Ellis'
]);
// update the record with id = 10
$result = $personGateway->update(10, [
'firstname' => 'Doug',
'lastname' => 'Ellis',
'secondparent_id' => 1
]);
// delete the record with id = 10
$result = $personGateway->delete(10);
实现 PHP REST API
现在我们将实现一个 REST API,它具有以下端点:
// return all records GET /person // return a specific record GET /person/{id} // create a new record POST /person // update an existing record PUT /person/{id} // delete an existing record DELETE /person/{id}
我们将创建一个 /public/index.php 文件作为我们的前端控制器并处理请求,以及一个 src/Controller/PersonController.php 文件来处理 API 端点(在验证 URI 后从前端控制器调用)。
public/index.php
<?php
require "../bootstrap.php";
use Src\Controller\PersonController;
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri );
// all of our endpoints start with /person
// everything else results in a 404 Not Found
if ($uri[1] !== 'person') {
header("HTTP/1.1 404 Not Found");
exit();
}
// the user id is, of course, optional and must be a number:
$userId = null;
if (isset($uri[2])) {
$userId = (int) $uri[2];
}
$requestMethod = $_SERVER["REQUEST_METHOD"];
// pass the request method and user ID to the PersonController and process the HTTP request:
$controller = new PersonController($dbConnection, $requestMethod, $userId);
$controller->processRequest();
src/Controller/PersonController.php
<?php
namespace Src\Controller;
use Src\TableGateways\PersonGateway;
class PersonController {
private $db;
private $requestMethod;
private $userId;
private $personGateway;
public function __construct($db, $requestMethod, $userId)
{
$this->db = $db;
$this->requestMethod = $requestMethod;
$this->userId = $userId;
$this->personGateway = new PersonGateway($db);
}
public function processRequest()
{
switch ($this->requestMethod) {
case 'GET':
if ($this->userId) {
$response = $this->getUser($this->userId);
} else {
$response = $this->getAllUsers();
};
break;
case 'POST':
$response = $this->createUserFromRequest();
break;
case 'PUT':
$response = $this->updateUserFromRequest($this->userId);
break;
case 'DELETE':
$response = $this->deleteUser($this->userId);
break;
default:
$response = $this->notFoundResponse();
break;
}
header($response['status_code_header']);
if ($response['body']) {
echo $response['body'];
}
}
private function getAllUsers()
{
$result = $this->personGateway->findAll();
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode($result);
return $response;
}
private function getUser($id)
{
$result = $this->personGateway->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = json_encode($result);
return $response;
}
private function createUserFromRequest()
{
$input = (array) json_decode(file_get_contents('php://input'), TRUE);
if (! $this->validatePerson($input)) {
return $this->unprocessableEntityResponse();
}
$this->personGateway->insert($input);
$response['status_code_header'] = 'HTTP/1.1 201 Created';
$response['body'] = null;
return $response;
}
private function updateUserFromRequest($id)
{
$result = $this->personGateway->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$input = (array) json_decode(file_get_contents('php://input'), TRUE);
if (! $this->validatePerson($input)) {
return $this->unprocessableEntityResponse();
}
$this->personGateway->update($id, $input);
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = null;
return $response;
}
private function deleteUser($id)
{
$result = $this->personGateway->find($id);
if (! $result) {
return $this->notFoundResponse();
}
$this->personGateway->delete($id);
$response['status_code_header'] = 'HTTP/1.1 200 OK';
$response['body'] = null;
return $response;
}
private function validatePerson($input)
{
if (! isset($input['firstname'])) {
return false;
}
if (! isset($input['lastname'])) {
return false;
}
return true;
}
private function unprocessableEntityResponse()
{
$response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
$response['body'] = json_encode([
'error' => 'Invalid input'
]);
return $response;
}
private function notFoundResponse()
{
$response['status_code_header'] = 'HTTP/1.1 404 Not Found';
$response['body'] = null;
return $response;
}
}
您可以使用 Postman 等工具测试 API。首先,进入项目目录并启动 PHP 服务器。
php -S 127.0.0.1:8000 -t public
然后使用 Postman 连接到 127.0.0.1:8000
并发送 HTTP 请求。注意:在进行 PUT 和 POST 请求时,请确保将 Body 类型设置为 raw
,然后以 JSON 格式粘贴负载,并将内容类型设置为 JSON (application/json)。
使用 OAuth 2.0 保护您的 PHP REST API
我们将使用 Okta 作为我们的授权服务器,并将实现客户端凭据流。该流推荐用于机器对机器身份验证,当客户端是私有时,其工作方式如下:客户端应用程序持有客户端 ID 和密钥;客户端将这些凭据传递给 Okta 并获取访问令牌;客户端将访问令牌发送到 REST API 服务器;服务器向 Okta 查询一些元数据,这些元数据允许它验证令牌并验证令牌(或者,它也可以直接让 Okta 验证令牌);如果令牌有效,服务器将提供 API 资源,否则,如果令牌缺失、过期或无效,则响应 401 Unauthorized 状态码。
在继续之前,您需要登录您的 Okta 帐户(或 免费创建一个新的帐户),创建您的授权服务器并设置您的客户端应用程序。
登录到您的开发者控制台,导航到 API,然后到 Authorization Servers 选项卡。单击您的默认服务器的链接。我们将从此 Settings 选项卡复制 Issuer Uri 字段并将其添加到我们的 .env 文件中。
OKTAISSUER=https://{yourOktaDomain}/oauth2/default
您可以在上面的屏幕截图中看到我的测试 Okta 帐户的 Issuer URI。复制您自己的值并将其放入您的 .env
文件中。
接下来,单击 Edit 图标,转到 Scopes 选项卡,然后单击 Add Scope 为 REST API 添加一个范围。我们将将其命名为 person_api
。
我们还需要将此范围添加到我们的 .env 文件中。我们将向 .env.example 添加以下内容:
SCOPE=
并将键和值添加到 .env 中。
SCOPE=person_api
下一步是创建一个客户端。导航到 Applications,然后单击 Add Application。选择 Service,然后单击 Next。为您的服务输入一个名称(例如,People Manager),然后单击 Done。这将带您到一个包含您的客户端凭据的页面。
这些是您的客户端应用程序需要进行身份验证的凭据。在此示例中,客户端和服务器代码将在同一个存储库中,因此我们也将这些凭据添加到我们的 .env
文件中(请确保将 {yourClientId}
和 {yourClientSecret}
替换为来自此页面的值)。
添加到 .env.example
OKTACLIENTID= OKTASECRET=
添加到 .env
OKTACLIENTID={yourClientId} OKTASECRET={yourClientSecret}
为您的 PHP REST API 添加身份验证
我们将使用 Okta JWT Verifier 库。它需要一个 JWT 库(我们将使用 spomky-labs/jose)和一个符合 PSR-7 的库(我们将使用 guzzlehttp/psr7)。我们将通过 composer 安装所有内容。
composer require okta/jwt-verifier spomky-labs/jose guzzlehttp/psr7
现在我们可以将授权代码添加到我们的前端控制器(如果使用框架,我们将在此处将其添加到中间件中)。
public/index.php (完整版,以便清晰理解)
<?php
require "../bootstrap.php";
use Src\Controller\PersonController;
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode('/', $uri);
// all of our endpoints start with /person
// everything else results in a 404 Not Found
if ($uri[1] !== 'person') {
header("HTTP/1.1 404 Not Found");
exit();
}
// the user id is, of course, optional and must be a number:
$userId = null;
if (isset($uri[2])) {
$userId = (int) $uri[2];
}
// authenticate the request with Okta:
if (! authenticate()) {
header("HTTP/1.1 401 Unauthorized");
exit('Unauthorized');
}
$requestMethod = $_SERVER["REQUEST_METHOD"];
// pass the request method and user ID to the PersonController:
$controller = new PersonController($dbConnection, $requestMethod, $userId);
$controller->processRequest();
function authenticate() {
try {
switch(true) {
case array_key_exists('HTTP_AUTHORIZATION', $_SERVER) :
$authHeader = $_SERVER['HTTP_AUTHORIZATION'];
break;
case array_key_exists('Authorization', $_SERVER) :
$authHeader = $_SERVER['Authorization'];
break;
default :
$authHeader = null;
break;
}
preg_match('/Bearer\s(\S+)/', $authHeader, $matches);
if(!isset($matches[1])) {
throw new \Exception('No Bearer Token');
}
$jwtVerifier = (new \Okta\JwtVerifier\JwtVerifierBuilder())
->setIssuer(getenv('OKTAISSUER'))
->setAudience('api://default')
->setClientId(getenv('OKTACLIENTID'))
->build();
return $jwtVerifier->verify($matches[1]);
} catch (\Exception $e) {
return false;
}
}
构建一个示例客户端应用程序(命令行脚本)来测试 PHP REST API
在本节中,我们将添加一个简单的客户端应用程序(使用 curl 的命令行脚本)来测试 REST API。我们将创建一个新的 php 文件 'public/clients.php',它具有一个非常简单的流程:它将从 .env 文件中检索 Okta 详细信息(issuer、scope、client id 和 secret),然后它将从 Okta 获取访问令牌,然后它将运行 API 调用来获取所有用户并获取特定用户(在 Authorization 头中传递 Okta 访问令牌)。
public/client.php
<?php
require "../bootstrap.php";
$clientId = getenv('OKTACLIENTID');
$clientSecret = getenv('OKTASECRET');
$scope = getenv('SCOPE');
$issuer = getenv('OKTAISSUER');
// obtain an access token
$token = obtainToken($issuer, $clientId, $clientSecret, $scope);
// test requests
getAllUsers($token);
getUser($token, 1);
// end of client.php flow
function obtainToken($issuer, $clientId, $clientSecret, $scope) {
echo "Obtaining token...";
// prepare the request
$uri = $issuer . '/v1/token';
$token = base64_encode("$clientId:$clientSecret");
$payload = http_build_query([
'grant_type' => 'client_credentials',
'scope' => $scope
]);
// build the curl request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $uri);
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
"Authorization: Basic $token"
]);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// process and return the response
$response = curl_exec($ch);
$response = json_decode($response, true);
if (! isset($response['access_token'])
|| ! isset($response['token_type'])) {
exit('failed, exiting.');
}
echo "success!\n";
// here's your token to use in API requests
return $response['token_type'] . " " . $response['access_token'];
}
function getAllUsers($token) {
echo "Getting all users...";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:8000/person");
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: $token"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
var_dump($response);
}
function getUser($token, $id) {
echo "Getting user with id#$id...";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:8000/person/" . $id);
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: $token"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
var_dump($response);
}
您可以通过进入 /public 目录并运行以下命令从命令行运行应用程序:
php client.php
(如果您还没有启动服务器,请不要忘记启动它!)
php -S 127.0.0.1:8000 -t public
就是这样!
了解更多关于 PHP、安全 REST API 和 OAuth 2.0 客户端凭据流的信息
您可以在此处找到完整的代码示例:GitHub 链接
如果您想深入探讨本文涵盖的主题,以下资源是一个很好的起点。
喜欢今天学到的东西吗?在 Twitter 上关注我们,并订阅我们的 YouTube 频道 以获取更多精彩内容!