emteknetnz / silverstripe-rest-api
Silverstripe的Rest API
Requires
- php: ^7.4 || ^8.0
- silverstripe/framework: ^4 || ^5
Requires (Dev)
- phpunit/phpunit: ^9.6
- silverstripe/versioned: ^1 || ^2
README
注意:此模块目前为预发布版。API和/或行为可能会随着新预发布版本的标记而更改。
此模块允许您快速轻松地创建安全的REST API端点,这些端点可用于提供数据库记录作为JSON,以及可选地通过API更新数据。
简单继承RestApiEndpoint
类,并使用private static array
配置定义您的端点。
端点提供一种数据对象类型的数据,例如SiteTree
。要为更多数据对象类型提供数据,请添加更多端点。
如果您端点提供非数据对象数据,则此模块不打算取代常规Silverstripe控制器端点。
以下说明假设您已创建一个项目,其中在composer.json中包含silverstripe/recipe-cms
,因为一些代码说明包括SiteTree
类。您仍然可以使用此模块而不需要silverstripe/recipe-cms
,因为silverstripe/framework
是唯一的要求,尽管对于publish
、unpublish
和archive
操作需要silverstripe/versioned
。
还有一个常见的陷阱,如果您的数据对象未显示在JSON响应中,那么您可能需要添加一个返回true
的canView()
到您的数据对象中。或者,您可以直接在端点配置中将CALL_CAN_METHODS
设置为CREATE_EDIT_DELETE_ACTION
(缺少VIEW
)以禁用canView()
检查。
内容
安装
composer require emteknetnz/silverstripe-rest-api
在Silverstripe CMS 4和5上均有效。
快速入门
将以下代码片段复制粘贴以快速设置一个公开只读端点,提供SiteTree
数据。
这假设您已创建一个项目,其中已安装silverstripe/cms
,以便您可以使用SiteTree
类。
src/MySiteTreeEndpoint.php
<?php use emteknetnz\RestApi\Controllers\RestApiEndpoint; use SilverStripe\CMS\Model\SiteTree; class MySiteTreeEndpoint extends RestApiEndpoint { private static array $api_config = [ RestApiEndpoint::PATH => 'api/pages', RestApiEndpoint::DATA_CLASS => SiteTree::class, RestApiEndpoint::ACCESS => RestApiEndpoint::PUBLIC, RestApiEndpoint::FIELDS => [ 'title' => 'Title', 'absoluteLink' => 'AbsoluteLink', 'content' => 'Content', 'lastEdited' => 'LastEdited', ], ]; }
运行https://mysite.test/dev/build?flush=1
访问https://mysite.test/api/pages
以查看端点数据。
访问https://mysite.test/api/pages/1
以查看ID为1
的页面的端点数据。
查询数据
通过向端点发出的GET请求中添加查询字符串参数来过滤数据
过滤
按精确值进行过滤
?filter=[<字段>]=<值>
使用搜索过滤器,例如PartialMatchFilter
,使用
?filter=[<字段>:<SearchFilter>]=<值>
在查询字符串中使用搜索过滤器时,请从其名称中省略'Filter'后缀。例如,要使用StartsWithFilter
搜索以"Hello"开头的标题,请在查询字符串中使用StartsWith
?filter=[<标题>:StartsWith]=Hello
使用搜索过滤器修饰符,例如"case"使用
?filter=[<字段>:<SearchFilter>:<修饰符>]=<值>
例如,要返回包含单词“关于”的所有页面,且区分大小写。请注意,如果没有指定,Silverstripe ORM默认使用nocase
搜索修饰符。
?filter[title:PartialMatch:case]=About
要使用多个过滤器,例如根据title
和lastEdited
字段进行筛选
?filter[title:PartialMatch]=rockets&filter[lastEdited:GreaterThan]=2022-01-01
以下是一些可用的搜索过滤器
ExactMatch
StartsWith
EndsWith
PartialMatch
GreaterThan
GreaterThanOrEqual
LessThan
LessThanOrEqual
以下是一些可用的搜索过滤器修饰符
case
no-case
not
排序
按升序排序字段
?sort=<field>
按降序排序字段
?sort=-<field>
要按多个字段排序,请使用逗号分隔它们
?sort=-<field1>,<field2>
例如,要按发布年份降序排序,然后按标题升序排序
?sort=-publishedYear,title
限制和偏移
限制记录数量
?limit=<number>
偏移记录
?offset=<number>
例如,获取10条记录的第二页
?limit=10&offset=10
默认限制为30
,通过查询字符串指定的最大限制为100
。这两个限制都可以在端点配置中更改。
HTTP请求和状态码
失败代码
以下失败代码在各种请求中使用
响应体将是包含"success":false
节点和描述错误的"message"
节点的JSON。
OPTIONS
OPTIONS
HTTP请求始终允许,并将返回端点的允许操作列表,位于allow
响应头中
GET
GET
HTTP请求用于从端点读取数据。您可以通过访问端点URL来查看节点列表,或通过访问带有节点ID的端点URL来查看单个节点。例如。
示例
curl -X GET https://mysite.test/api/pages
curl -X GET https://mysite.test/api/pages/<id>
HEAD
与GET
相同,但只返回GET
将返回的头部,没有正文
示例
curl --head https://mysite.test/api/pages
curl --head https://mysite.test/api/pages/123
POST
POST
HTTP请求用于创建新记录。请求体应是一个JSON对象,包含创建记录所需的数据,该数据与端点配置匹配,即指定要更新的jsonKey
,而不是dataObjectKey
。
指定字段值是可选的,尽管可能需要根据DataObject验证。
示例
curl -X POST https://mysite.test/api/pages -d '{"title":"My title"}'
PATCH
PATCH
HTTP请求用于更新指定在URL中的ID的现有记录。请求体应是一个JSON对象,包含更新记录所需的数据,该数据与端点配置匹配,即指定要更新的jsonKey
,而不是dataObjectKey
。
指定字段值是可选的,尽管可能需要根据DataObject验证。
示例
curl -X PATCH https://mysite.test/api/pages/123 -d '{"title":"My updated title"}'
DELETE
DELETE
HTTP 请求用于删除在 URL 中指定 ID 的现有记录。如果已经将 silverstripe/versioned
模块中的 Versioned
扩展应用到 DataObject 上,那么就会在 DataObject 上调用 doArchive()
方法,将其从网站的草稿版和实时版本中删除。如果没有应用 Versioned
扩展,那么就会在 DataObject 上调用 delete()
方法。
示例
curl -X DELETE https://mysite.test/api/pages/123
PUT
PUT
HTTP 请求仅用于执行预定义的一系列操作。它不用于创建或更新数据,这在其他 REST API 实现中通常用 PUT
来执行。相反,对于创建操作使用 POST
,对于更新操作使用 PATCH
。
操作只能对现有记录执行。操作参数添加到现有记录的 ID 之后。
以下是一些可用的操作
示例
curl -X PUT https://mysite.test/api/pages/123/publish
端点配置选项
端点配置是通过 RestApiEndpoint
子类上的 private array static $api_config
字段来完成的。请记住使用 ?flush=1
来应用新的配置。以下表格包含 RestApiEndpoint
类上可用的配置常量列表。
关系
您的端点可以包含数据对象上的关系数据,例如 has_one
、has_many
或 many_many
关系。关系的配置遵循与顶级配置相同的规则。
例如,有一个名为 Team
的类,它有一个 db
字段 Title
和一个 has_many
关系 Players
,而 Player
类有一个 db
字段 LastName
,您可以使用以下端点配置来显示每个 Team
上的所有 Players
。
请注意,当包含关系时,由于无法分页关系数据,所以所有关系都会包含在响应中,而不是默认的 30
条限制。
private static array $api_config = [ RestApiEndpoint::PATH = 'api/teams'; RestApiEndpoint::DATA_CLASS => Team::class, RestApiEndpoint::FIELDS => [ // db fields 'title' => 'Title', 'yearFounded' => 'YearFounded', // has_one relation 'city' => [ RestApiEndpoint::RELATION => 'City', RestApiEndpoint::FIELDS => [ 'name' => 'Name', ], ], ], // has_many relation 'players' => [ RestApiEndpoint::RELATION => 'Players', RestApiEndpoint::FIELDS => [ 'lastName' => 'LastName, ], ], ], ];
可以在端点配置中定义的 has_one
关系可以通过使用魔法字段 <jsonField>__ID
通过 POST
或 PATCH
请求进行设置。
例如,要更新具有 ID
为 77
的现有 Team
数据对象上的 CityID
从 14
到 15
。
curl -X PATCH https://mysite.test/api/teams/77 -d '{"city__ID":"15"}'
单个字段
包含字段的正常表示法是 <jsonField>
=> <dataObjectField>
。如果您希望限制这些字段,则可以配置单个字段以拥有自己的 ACCESS
和 ALLOWED_OPERATIONS
。执行此操作时,表示法会更改到数组表示法,其中 DATA_OBJECT_FIELD
是常规表示法中 <dataObjectField>
的 DATA_OBJECT_FIELD
。
例如,要将 Team
类上的 PrivateField
设置为仅对通过自定义 CAN_ACCESS_PRIVATE_FIELD
权限检查的登录成员可访问
private static array $api_config = [ RestApiEndpoint::PATH = 'api/teams'; RestApiEndpoint::DATA_CLASS => Team::class, RestApiEndpoint::FIELDS => [ 'title' => 'Title', 'privateField' => [ RestApiEndPoint::DATA_OBJECT_FIELD => 'PrivateField', RestApiEndpoint::ACCESS => 'CAN_ACCESS_PRIVATE_FIELD', ], ], ];
CSRF 令牌
如果端点的 ACCESS
设置为除了 PUBLIC
之外的内容,那么请求需要发送一个 x-csrf-token
标头,除非发送了 x-api-token
。有效的令牌由 SecurityToken::getSecurityID()
生成。在 CMS 中,它可以通过 window.ss.config.SecurityID;
获取。
例如,以下 JavaScript 代码将在登录 Silverstripe CMS 时包含 x-csrf-token
标头的 GET
请求。
fetch( '/api/pages', { headers: { 'x-csrf-token': window.ss.config.SecurityID } } ) .then(response => response.json()) .then(responseJson => console.log(responseJson));
当与非公开API端点一起工作时,您可能希望禁用csrf令牌检查,以便您可以在浏览器的地址栏中快速测试GET
查询。您可以通过在app/_config.php
中调用SecurityToken::disable()
来完成此操作,尽管这样做时要非常小心,以免在生产环境中也被禁用。为了安全起见,您可以将此操作包装在一个检查您选择的设置的环境变量的检查中,您可以在您的本地.env文件中设置该变量,例如
use SilverStripe\Core\Environment; use SilverStripe\Security\SecurityToken; // ... if (Environment::getEnv('DISABLE_API_CSRF_TOKEN_CHECK')) { SecurityToken::disable(); }
x-csrf-token
头在RestApiEndpoint::CSRF_TOKEN_HEADER
上作为常量可用。
API令牌
非公开API可以配置为允许成员使用HTTP头进行身份验证,而不是必须登录到CMS。
如果使用API身份验证,用户将仅在请求期间登录,即他们在返回JSON响应之前将被注销。
本模块提供了一个“使用API令牌”权限,即API_TOKEN_AUTHENTICATION
,必须将其分配给使用API令牌的用户所属的组。必须将端点配置ALLOW_API_TOKEN
设置为true
。
当用户和端点设置允许使用API令牌时,传递一个包含API令牌值的x-api-token
头以进行身份验证。请注意,如果为该用户设置了多因素身份验证(MFA),则API令牌身份验证将绕过MFA。
使用CMS设置API用户和组
创建API用户和组
- 以管理员身份登录到CMS
- 转到安全部分
- 创建一个名为“API用户”的新组
- 单击权限选项卡(右上角)
- 勾选“使用API令牌” - 这是权限代码
API_TOKEN_AUTHENTICATION
的标签 - 保存组
- 单击“添加成员”
- 创建一个名为“api-user”的新用户,电子邮件为"api-user@example.com",并使用一个长随机密码
- 将其分配给“Api Users”组
- 勾选“生成新的API令牌”复选框,然后单击“保存”
- 复制生成的API令牌 - 您只会看到一次
其他组权限
“api-user”仍然需要通过所有必要的权限检查,以便API可以正常工作,即canView()
检查仍然通过。您可以选择以下两种方法之一
- 更新“API Users”组以具有必要的权限,或者
- 将端点配置
CHECK_CAN_METHODS
设置为NONE
,但您必须确保API的ACCESS
设置为仅分配给专用API用户的权限代码。
以编程方式更新用户的API令牌
使用$member->refreshApiToken();
以编程方式更新用户的API令牌,然后使用$member->write();
。返回值是未加密的API令牌。成员的ApiToken
字段将是加密的API令牌。
请注意,对于新创建的用户,必须在调用$member->refreshApiToken();
之前至少调用一次$member->write()
,以确保API令牌被正确加密。
扩展钩子
您可能需要向API添加自定义逻辑,可以使用以下表中的可用扩展钩子。通过将以下方法之一直接添加到您的RestApiEndpoint
子类中并使用protected
可见性来实现钩子。您也可以使用public
可见性在扩展类中实现它们。
例如,以下对onEditBeforeWrite()
钩子的实现将在保存之前更新通过PATCH
请求更新的数据对象的Content
字段,即使该Content
字段没有在API中公开。
请注意,要运行此代码示例,您需要登录到CMS以使用它,并在发出请求时传递一个x-csrf-token
头。
src/MySiteTreeEndpoint.php
<?php use emteknetnz\RestApi\Controllers\RestApiEndpoint; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\ORM\DataObject; class MySiteTreeEndpoint extends RestApiEndpoint { private static array $api_config = [ RestApiEndpoint::PATH => 'api/pages', RestApiEndpoint::DATA_CLASS => SiteTree::class, RestApiEndpoint::ACCESS => RestApiEndpoint::LOGGED_IN, RestApiEndpoint::ALLOWED_OPERATIONS => RestApiEndpoint::VIEW_CREATE_EDIT_DELETE_ACTION, RestApiEndpoint::FIELDS => [ 'title' => 'Title', ], ]; protected function onEditBeforeWrite(SiteTree $page) { // You wouldn't normally do this, this is only for demo purposes $page->Content .= '<p>This was updated using the API</p>'; } }
注意事项
- 如果你的扩展钩子更新了DataObject或其他DataObject,那么你很可能应该使用不同的扩展钩子,例如在DataObject本身上使用
onAfterWrite()
,而不是在端点上。这是因为通常情况下,对象是通过API或另一种方式创建/更新/删除的,这通常无关紧要。这些钩子旨在便于实现API特定的代码,例如记录通过API执行的操作。 - 对于
onView*()
钩子,如果你正在为响应JSON添加额外数据,请记住,对于任何被添加的DataObject,都要调用canView()
。 - 对于这两个
onEdit*Write()
钩子,$changedFields
参数是对象写入之前,通过$obj->getChangedFields()
返回的值。