sgalinski/sg-rest

该扩展提供了一个基本的REST环境。新端点提供了REST环境,这样其他扩展只需要注册它们即可。

安装: 1

依赖项: 0

建议者: 0

安全: 0

类型:typo3-cms-extension

6.0.0 2024-01-19 17:40 UTC

README

许可协议: GNU GPL, 版本 2

仓库: https://gitlab.sgalinski.de/typo3/sg_rest

请在此处报告错误: https://gitlab.sgalinski.de/typo3/sg_rest

如何调用REST函数?

使用Chrome和Postman

在这种情况下,您可以安装Chrome扩展程序"Postman"(https://www.getpostman.com/)。使用此扩展程序,您可以向特定URL发送带有POST参数的REST调用,这是我们的REST实现所必需的。

REST注册

目标

在此注册过程之后,您可以调用以下REST函数

Calls an action of the entity, or returns an entity with the given uid.

URL: https://www.website-base.dev/?type=1595576052&request=<apiKey>/<entityName>/<actionOrUid>

Required POST data:

authToken = <aAuthTokenFromAUser> // See "REST Authentication" for this

OR

bearerToken = <bearerToken> // See "REST Authentication" for this

任务

1) 在扩展的"ext_localconf.php"中调用此函数

$class = 'SGalinski\SgRest\Service\RegistrationService';
$restRegistrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($class);
$restRegistrationService->registerAccessGroup(
	<apiKey>, // Example: "news"
	'Vendor.<extension_key>', // Example: "Vendor.sg_news"
	<accessGroupName>, // Example: "News" It's the name of the api, which is shown in the user TCA. See: "REST Authentication"
	[
		<entityName> => [ // Example: "news"
			'read' => 'uid, title' // This allows that the API can read the fields "uid" and "title" from the entity "news"
		]
	]
);

2) 创建一个控制器,它是注册的端点

namespace Vendor\ExtensionName\Controller\Rest\<apiKeyWithCamelcase>; // Example: "...\Controller\Rest\News"

use SGalinski\SgRest\Controller\AbstractRestController;
use SGalinski\SgRest\Service\PaginationService;
use SGalinski\SgRest\Domain\Model\FrontendUser;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings;

class <entityNameWithCamelcase>Controller extends AbstractRestController { // Example: "NewsController"
	/**
	 * Class name for the model mapping
	 *
	 * @var string
	 */
	protected $className = <entityNameWithNameSpace>; // Example: "Vendor\ExtensionName\Domain\Model\News"

	/**
	 * @var EntityRepository
	 */
	protected $entityRepository;

	/**
	 * Injects the repository. Is lot faster to use the inject method than the inject annotation!
	 *
	 * @param EntityRepository $entityRepository
	 * @return void
	 */
	public function injectEntityRepository(EntityRepository $entityRepository) {
		/** @var $querySettings Typo3QuerySettings */
		$querySettings = GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings');
		$querySettings->setRespectStoragePage(FALSE);
		$entityRepository->setDefaultQuerySettings($querySettings);

		$this->entityRepository = $entityRepository;
	}

	/**
	 * Action to return an entity in json format.
	 *
	 * @param Entity $entity
	 * @return void
	 * @throws \Exception
	 */
	public function getAction(Entity $entity) {
		if (!$entity) {
			throw new \InvalidArgumentException('You´re not allowed to access this entity!', 403);
		}

		$this->returnData($this->dataResolveService->getArrayFromObject($entity));
	}

	/**
	 * Get list request for entities.
	 *
	 * @param int $page
	 * @param int $limit
	 * @throws \Exception
	 * @return void
	 */
	public function getListAction($page = 1, $limit = 10) {
		/** @var FrontendUser $authenticatedUser */
		$authenticatedUser = $this->authenticationService->getAuthenticatedUser();

		if (!$authenticatedUser) {
			throw new \InvalidArgumentException('You´re not allowed to access this entity list!', 403);
		}

		$response = ['amountOfAllEntries' => 0, 'prev' => NULL, 'next' => NULL, 'data' => []];
        $amountOfAllEntities = $this->entityRepository->countAll();

        /** @var PaginationService $paginationService */
        $class = 'SGalinski\SgRest\Service\PaginationService';

        $paginationService = GeneralUtility::makeInstance($class, $page, $limit, 10, $amountOfAllEntities, $this->apiKey);
        $paginationSettings = $paginationService->getPaginationSettings();
        $response['prev'] = $paginationService->getPreviousPageUrl(<entityName>); // Example: "news"
        $response['next'] = $paginationService->getNextPageUrl(<entityName>); // Example: "news"
        $response['amountOfAllEntries'] = $amountOfAllEntities;

        $entries = $this->entityRepository->findAllWithPagination($limit, $paginationSettings['offset']);
        foreach ($entries as $entry) {
            $response['data'][] = $this->dataResolveService->getArrayFromObject($entry);
        }

		$this->returnData($response);
	}
}

3) 在您的实体仓库中创建函数"findAllWithPagination"

	/**
	 * Find all entries with some pagination information.
	 *
	 * @param int $limit
	 * @param int $offset
	 * @return array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface
	 */
	public function findAllWithPagination($limit, $offset) {
		return $this->createQuery()->setOrderings(['crdate' => QueryInterface::ORDER_ASCENDING])
			->setOffset((int) $offset)
			->setLimit((int) $limit)
			->execute();
	}

额外的注册配置

如果您想使用POST/PUT/PATCH/DELETE请求,则需要添加额外的配置"httpPermissions"。

$class = 'SGalinski\SgRest\Service\RegistrationService';
$restRegistrationService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($class);
$restRegistrationService->registerAccessGroup(
	<apiKey>, // Example: "news"
	'Vendor.<extension_key>', // Example: "Vendor.sg_news"
	<accessGroupName>, // Example: "News" It's the name of the api, which is shown in the user TCA. See: "REST Authentication"
	[
		<entityName> => [ // Example: "news"
            'classFQN' => Vendor\ExtensionName\Controller\Rest\<apiKeyWithCamelcase>\<entityNameWithCamelcase>Controller::class,
			'read' => 'uid, title' // This allows that the API can read the fields "uid" and "title" from the entity "news",
			'httpPermissions' => [
				'deleteForVerbs' => TRUE,
				'putWithIdentifier' => TRUE,
				'patchWithIdentifier' => TRUE,
				'postWithIdentifier' => TRUE,
			],
		]
	]
);

提示

  1. 在您的控制器函数中始终使用函数"$this->returnData"来返回数据。
  2. 始终使用函数"$this->dataResolveService->getArrayFromObject($entity)"从您注册的实体获取数据。这尊重在"$restRegistrationService->registerAccessGroup()"中配置的允许字段。
  3. 在每个api函数中应使用缓存。
  4. 尽量不使用Extbase调用,这样性能会更好。

REST认证

我们的REST解决方案与前端用户的授权一起工作。为此,它为这项任务提供了两个新的TCA字段。

认证令牌字段

此字段必须设置为唯一的ID,因此可以与提供的"authToken"参数映射,该参数必须在每次REST函数调用中设置。

访问组字段

列出所有可用的注册访问组。每个组都与一个REST api相关联。如果用户在此处设置了组,则用户可以访问API。

使用Bearer Token代替认证令牌

作为认证令牌的替代方案,我们的REST解决方案还提供了通过Bearer Token / JWT进行认证。认证成功后,用户会获得一个令牌,然后必须在每次请求中发送该令牌。如果令牌有效,服务器将执行所需请求。由于此类令牌可以在无需数据库连接的情况下进行验证,因此当在认证服务/服务器可能成为瓶颈的微服务环境中使用时,它特别有趣。

使用BearerAuthenticationService

为了实际使用Bearer Token进行认证,我们需要将BasicAuthenticationService的使用切换到BearerAuthenticationService。使用AuthenticationService的类通过AuthenticationServiceInterface的构造函数依赖注入获得它,这意味着任何实现此接口的AuthenticationService都可以注入此处。默认情况下,为该接口配置的Services.yaml中的别名是BasicAuthenticationService。要切换到使用BearerAuthenticationService,您需要更改您的Services.yaml中的此别名

默认

SGalinski\SgRest\Service\Authentication\AuthenticationServiceInterface: '@SGalinski\SgRest\Service\Authentication\BasicAuthenticationService'

要使用BearerAuthenticationService

SGalinski\SgRest\Service\Authentication\AuthenticationServiceInterface: '@SGalinski\SgRest\Service\Authentication\BearerAuthenticationService'

如何获取Bearer Token

要获取Bearer Token,您需要执行一个身份验证请求,并将有效的用户凭据传输到以下URL

/?type=1595576052&tx_sgrest[request]=authentication/authentication/getBearerToken&logintype=login

由于我们使用基于中间件TYPO3\CMS\Frontend\Middleware\FrontendUserAuthenticator的标准身份验证过程,因此请求必须以POST方式执行,并包含表单数据参数userpass

当用户身份验证成功,并且用户至少被允许使用一个REST端点时,将返回Bearer Token

{
    "bearerToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoyLCJleHAiOjE2MDgwMjQzMTd9.exQ3UkeBBd2P_1o4woP0uBn6koxvTV63aVT13JTkWRw"
}

实现自己的AuthenticationService

您也可以实现自己的AuthenticationService。为此,您需要构建一个类,该类可以实现AuthenticationServiceInterface接口或扩展AbtstractAuthenticationService。

namespace Vendor\Extension\Service\Authentication;

use TYPO3\CMS\Core\SingletonInterface;
use SGalinski\SgRest\Service\AbstractAuthenticationService;

class CustomAuthenticationService extends AbstractAuthenticationService implements SingletonInterface {

	/**
	 * @param array $requestHeaders
	 * @return bool
	 * @throws Exception
	 */
	public function verifyRequest(array $requestHeaders): bool {
        // verify Request
        //return bool;
	}

	/**
	 * Verify if the authenticated user has access to the given apikey.
	 *
	 * @param $apiKey
	 * @return bool
	 */
	public function verifyUserAccess($apiKey): bool {
        // verify user access
        //return bool;
	}
}

附加配置

更好的调用URL

带有API子域

Old: https://www.website-base.dev/?type=1595576052&tx_sgrest[request]=<apiKey>/<entityName>/<actionOrUid>
New: https://api.website-base.dev/<apiKey>/<entityName>/<actionOrUid>

如果您想使用上述类似URL调用REST api,那么您只需将以下代码添加到项目的 .htaccess 文件中

# Api redirects
RewriteCond %{HTTP_HOST} ^api.(?:website-base.dev?)$ [NC]
RewriteRule ^(.+)$ index.php?type=1595576052&tx_sgrest[request]=%{REQUEST_URI} [QSA,NC,L]

不带API子域

Old: https://www.website-base.dev/?type=1595576052&tx_sgrest[request]=<apiKey>/<entityName>/<actionOrUid>
New: https://www.website-base.dev/api/v1/<apiKey>/<entityName>/<actionOrUid>

如果您想使用上述类似URL调用REST api,那么您需要将以下代码添加到 .htaccess 文件中

# Api redirects
RewriteRule ^api/v1/(.*) /index.php?type=1595576052&tx_sgrest[request]=$1 [QSA]

有关更多信息,请参阅下一节“自定义REST URL模式”

自定义REST URL模式

如果您的REST URL不是子域,您可能在主机后有一个URL段或完全不同。默认URL模式类似于HOST/APIKEY/ENTITY,如果您的宿主不包含API的标识符,您需要调整REST URL模式。否则,您的分页服务将生成无效的下一个和上一个URL。

您不需要添加URL方案。HTTPS是必需的,并且始终使用。URL模式使用handlebars进行标记。以下标记存在

  • {{HOST}}
  • {{APIKEY}}
  • {{ENTITY}}

因此,默认模式是:{{HOST}}/{{APIKEY}}/{{ENTITY}}

如果您想将URL从https://api.yourdomain.com/apikey/entity调整为https://www.yourdomain.com/api/apikey/entity等,您需要将新的URL模式设置为分页服务。

	/** @var PaginationService $paginationService */
	$paginationService = GeneralUtility::makeInstance(
		PaginationService::class, $page, $limit, 10, $amountOfEntries, $apiKey
	);
	$paginationService->setUrlPattern('{{HOST}}/api/{{APIKEY}}/{{ENTITY}}');

路由增强器

遗憾的是,由于PathUtility提供的映射到Rest Controller和Action的路径在请求参数中,因此该扩展的URL模式目前与路由增强器不兼容

[...]?sg_rest[request]=news/news/getList

路由增强器无法正确映射此内容。

在此期间,请使用上述解释的 .htaccess 选项美化您的REST URL!

日志记录和垃圾回收

此扩展使用典型的TYPO3日志机制。默认情况下,它使用数据库和表tx_sgrest_log。建议配置一个清理任务。如果您使用的是默认清除所有表的配置,则日志条目将每30天清除一次。