stratoss / phalcon2rest
具有OAuth2、JWT和速率限制的Phalcon2项目
Requires
- league/oauth2-server: 5.*
This package is auto-updated.
Last update: 2024-08-26 19:22:38 UTC
README
一个使用Phalcon2框架的API的基础项目
本项目是基于cmoore4的PhalconRest分支,但已修改以正确与Phalcon2协同工作。目前包含最新的phpleague的OAuth2 Server(5.x)和JSON Web Tokens (JWT)。同时实现了速率限制。
Phalcon框架是一个优秀的PHP框架,它作为语言的一个C扩展存在,这使得它非常快。除了它的速度之外,它还是一个功能强大的框架,具有优秀的文档,遵循了许多现代软件开发的最佳实践。这包括使用直接注入模式来处理类之间的服务解析、遵循PSR-0的自动加载器、MVC架构(或不是)、数据库、flatfile、redis等的缓存处理器以及许多其他附加功能。
本项目旨在建立一个使用Phalcon的基础项目,该项目使用Phalcon框架的最佳实践来实施API设计的最佳实践。
在任何主要框架中编写返回JSON的路由都很简单。我在这里所做的不仅仅是这一点,而是扩展了框架,使得使用此项目编写的API实际上是REST-like的,并实现了比简单的'echo json_encode($array)'更方便的方法和模式。
提供了健壮的错误消息、解析搜索字符串和部分响应的控制器、根据请求发送多种MIME类型的响应类,以及如何以几种方式实现身份验证的示例,以及一些用于实现常见REST-like任务的模板。
强烈建议阅读index.php
、Exceptions\HttpException.php
和Modules\V1\Controllers\RestController.php
文件。
有关使用OAuth2服务器的一般信息和完整文档,请访问此处
API假设
URL结构
/v1/path1/path2?q=(search1:value1,search2:value2)&fields=(field1,field2,field3)&limit=10&offest=20&type=csv&suppress_error_codes=true
请求体
请求体将以有效的JSON提交。
字段
搜索
搜索由'q'参数确定。之后是一个括号包围的键:值对列表,由逗号分隔。
示例:q=(name:Jonhson,city:Oklahoma)
部分响应
部分响应用于仅返回记录的某些特定字段。它们由'fields'参数确定,该参数是一个字段名称列表,由逗号分隔,并括在括号中。
示例:fields=(id,name,location)
限制和偏移量
常用于分页大型结果集。偏移量是要开始的记录,限制是要返回的记录数。
示例:limit=20&offset=20将返回第21至40条记录
返回类型
覆盖任何接受头。默认为JSON。必须实现返回类型处理程序。
示例:type=csv
抑制的错误代码
某些客户端要求所有响应均为200状态码(例如Flash),即使存在应用错误。包含此参数后,应用将始终返回200状态码,客户端将负责检查响应体以确保响应有效。
例如:suppress_error_codes=true
安装
获取composer
curl -sS getcomposer.org/installer | php
安装项目和依赖项(预期phalcon2作为模块加载!)
php composer.phar create-project stratoss/phalcon2rest MyAPI --stability dev --no-interaction
用于JWT签名的公钥/私钥 示例密钥在ssl
文件夹中生成,在进入生产之前必须重新生成您自己的密钥集!
openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout -out public.key
响应
所有路由控制器必须返回一个数组。此数组用于创建响应对象。
使用密码授权检索访问令牌
curl https://domain/v1/access_token --data "grant_type=password&client_id=1&client_secret=pass2&username=stan&password=pass&scope=basic"
{
"tokenType": "Bearer",
"expiresIn": 3600,
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImUyY2ExYjhiM2ZlODRhM2Q2ZGFmZjZhNmFiZmQwMjJiNzQxMGYxOWNmODAzMWVkOThlNjc4YzVjN2Q3Mjc4MTk0NDFhYzMyNTc5OTM1OTIxIn0.eyJhdWQiOiIxIiwianRpIjoiZTJjYTFiOGIzZmU4NGEzZDZkYWZmNmE2YWJmZDAyMmI3NDEwZjE5Y2Y4MDMxZWQ5OGU2NzhjNWM3ZDcyNzgxOTQ0MWFjMzI1Nzk5MzU5MjEiLCJpYXQiOjE0NjI3MjkxMTgsIm5iZiI6MTQ2MjcyOTExOCwiZXhwIjoxNDYyNzMyNzE4LCJzdWIiOiIxIiwic2NvcGVzIjpbImJhc2ljIl19.VtKS7k1WiebSHiYwSrYJ9D8G90BQE81UkCNr8RI3-Ul3XaUw8kF1mMR-Q7YeO4pISUT48Gu5Sj6fTSsqF1n0Qz3axR-qcJstSwy_T0VZDNQYYdGGoXSRWabiIkLA50lbaPd8YGPLZO3IijgvyZxC3Miz7iUxxz1XOHvzHiDZP5s",
"refreshToken": "U8PSWA\/xs\/Qw7\/\/Do76KM61+R6tUQwoex4YmnH0HjUCjzDKR22mRvSSPzkgbuR6EqjaaAp\/QWdBcYjgZj5XOyXAH9LM7C\/uBr4YeLUeTvzf7SylqIlLOuKe6aOZzD+L5ztSNdMCzjCQ1PZSWtk+hm7Ik1lUfXe1A\/09qI+hfG+p5cXHvow26ZSWz42uYaoEV5MAh+VBvR2vWx4WrAa6JJ\/6QISJRu+KTUXJmiGBbzhZiHZJ7+pyTeLiFk\/euT8aTa2tIz5WMogQDhMfzzPadOZY88jrc9mPuwxOv8DPKnysAlYHklnEQzESoP4MmKP0D9yrbLx80eE3PdEnj1hPYcpLIb8Dvc1IK+rIzg9+71HSW5XH54npY7MlLbNPDAB02bdX5SDWJhT8XwN8zqZQiLOUwBgP\/ESjW5dI7ckLzmmguqRPbd3TtNoYGultyFUCIxKH1FVNqJrxgVqJk083KXqFAzcsZw6cxgvies3djGxcPGCzyNRlxuEU8+ZoMJ0u0"
}
使用客户端凭证授权检索访问令牌
curl https://domain/v1/access_token --data "grant_type=client_credentials&client_id=1&client_secret=pass2&scope=basic"
{
"tokenType": "Bearer",
"expiresIn": 3600,
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImQwYjU1YjJiMjhhYmIxOTMwODk3OTg2ZTIxN2RkMTkwZGY1YTlkYzk2NGJmZjljY2ZjMWQ0NGI4Y2RiNjU0OTAyYjMwM2M1ZDliMzY5ZWQxIn0.eyJhdWQiOiIxIiwianRpIjoiZDBiNTViMmIyOGFiYjE5MzA4OTc5ODZlMjE3ZGQxOTBkZjVhOWRjOTY0YmZmOWNjZmMxZDQ0YjhjZGI2NTQ5MDJiMzAzYzVkOWIzNjllZDEiLCJpYXQiOjE0NjI3MjE4NzcsIm5iZiI6MTQ2MjcyMTg3NywiZXhwIjoxNDYyNzI1NDc3LCJzdWIiOiIiLCJzY29wZXMiOlsiYmFzaWMiXX0.KDkaVBMBX4UelYJ4UoknjgrssEaqpQPj2MPe4ArIppIc0BYNA-5xxVWCSu8rSGKO7QAVM2XSyiux3yq8NoClgtaPlPtpZN6pcSfwGo9MSM6IwQanpd978pwPCi-tXXl4mlViph9sgxPioJ3CzCBoJTpeEtRnEm6nxMUgLnncXps"
}
交换刷新令牌以获取新的刷新令牌和访问令牌集
curl https://domain/v1/access_token --data "client_id=1&client_secret=pass2&grant_type=refresh_token&scope=basic&refresh_token=YOUR_REFRESH_TOKEN"
{
"tokenType": "Bearer",
"expiresIn": 3600,
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImY0NGRiMzQ1MzE0MDdlMWY2MWU0N2NkODQ2ZjIxOTRiMjNiZWZhNzZmOWVjYWY5ZDIyMWFhYTg5MTVhMDhjOGFhMzkzYTdmMGI4NGEwNjQ1In0.eyJhdWQiOiIxIiwianRpIjoiZjQ0ZGIzNDUzMTQwN2UxZjYxZTQ3Y2Q4NDZmMjE5NGIyM2JlZmE3NmY5ZWNhZjlkMjIxYWFhODkxNWEwOGM4YWEzOTNhN2YwYjg0YTA2NDUiLCJpYXQiOjE0NjI3MjIzMjIsIm5iZiI6MTQ2MjcyMjMyMiwiZXhwIjoxNDYyNzI1OTIyLCJzdWIiOiIxIiwic2NvcGVzIjpbImJhc2ljIl19.COJ5kAWEEjZyKN_k1N0sgiLJzpEtlgT9H3oJpeicQ-bZteuABZ3sYWCgBY2FrRm6Q8ouMra9WXj38NnwYRgOusRq2H1JL-3redvTu8LitPljNLYritSAuPivOVY4e6FVjQHeuXfIl37rmKIHUXmJcUSJRh1XOqW9mXJGggiXhlI",
"refreshToken": "muYBWN8fSzSL2UCQU0FCq7EZrJ7bPBmJxLsHOTzBSoHJn0gT+ilWyeJzvOqrlVJel4V8K7HIOQfExbKB5l0UrwzFo5UDCz5qj72wWgUn8aJWY09LfGZAs6Qsx\/INLmg6y7petQdtWspAPWlaid8OBk2w5IsqQ7kLFATHCA9fWIg3HWRrc8RPkWeBgOZ5ekRO1dGnmzDm+HLmt8hvIq7uiNDRINYYDwmYh50Ifkv8iJhxL7Pj351KPg43G9pB6L8mNfVizx71c3cofuHlTYMc4S5pt9PFBg7kbtR+qYAD5Wpm3jK204HTpx\/lYyVtEZuFou8O+7ssWlWCSXf7wogxPy9fMuRgXzONnqUn8XHDJEBOxZIIVeu7AAgsWKGJvNrLVY+81oa8BQL1MdCqxQs8vVnHgzq9+bnrjZPlhcvm\/jhWzeCx6X\/fjdneTsZXOZXLK0OpCYNkyOaT2xC5H3RI2+jRGU0HCXGJTmuBlz4Kx48fdUuy2DwF\/DL+LS2mWE6o"
}
使用隐式授权检索访问令牌
查看Modules/V1/Controllers/AuthorizeController.php
。必须采取额外步骤以验证用户。
为了简单起见,假设过程从对https://domain/v1/authorize
的POST请求开始,发送POST数据“response_type=token&client_id=1&scope=basic”,并成功进行客户端认证,则响应将重定向到
http://example.com/super-app#access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjBjZDUxMDRkZDg2YTg0OThhZWUyZGQzOGNlYzgzYzRkMTU4MmE4YjM4ZmZjYWY3ZDQ2MjZiZTY0NWUxN2Q0MjJjZWJmMDRlNWY2YjBjNWUxIn0.eyJhdWQiOiIxIiwianRpIjoiMGNkNTEwNGRkODZhODQ5OGFlZTJkZDM4Y2VjODNjNGQxNTgyYThiMzhmZmNhZjdkNDYyNmJlNjQ1ZTE3ZDQyMmNlYmYwNGU1ZjZiMGM1ZTEiLCJpYXQiOjE0NjI3ODg0NzcsIm5iZiI6MTQ2Mjc4ODQ3NywiZXhwIjoxNDYyNzkyMDc3LCJzdWIiOiIxIiwic2NvcGVzIjpbImJhc2ljIl19.mI8E7KVG6NGxBqbZ6nVojtOXbvRCQzjnNcBSRHAbF2SyoKQlQTGAfDmGNxozfKoNh7G60Il84NKYVvwhC3S3-jLhsEgVA0UePnVnGq4V84M0yMBtLJY3puLSIOAoAGuvUjMjSlxNJnqXZ68R3oD1vi3dmA7MVeSELbii2apAyo4&token_type=bearer&expires_in=3600
使用授权码授权检索访问代码
查看Modules/V1/Controllers/AuthorizeController.php
。必须采取额外步骤以验证用户。
为了简单起见,假设过程从对https://domain/v1/authorize
的POST请求开始,发送POST数据“response_type=code&client_id=1&scope=basic”,并成功进行客户端认证,则响应将重定向到
http://example.com/super-app?code=ReoVHgGRnMj6IVhAUDUvunKKCi2BvGxfsJ8nGMj%2FIO2ITr6u7%2FJ7epKAIEG%2F0KZMk5Cc5GhWouG8zYHgGwzAHSztOS%2FKKp8krH5rm6e4pIkmhYvy9TCDUF1fdSo0axfZTQm1V9Ja8Ww3GN%2BeMvpmoKCXPNB8VEOs7smkTI9EGJGjVC2bS26ZJKWGuIV1UqyUKEeSiNfvhAqzeZWF2fXhGDDxmawtIPo7C3Vhs9ZW035P%2FKcugRxdT5t5MTkB%2BgRllqNGLo1DCnXvSB9E9H6KOEraMMYdqzcX4YNX8TseBrJINBJM7JUZkjFqQ176DXfnI7ULN7R%2FUJrRwWNdPMuHwQ%3D%3D
JSON
JSON是默认的响应类型。响应将类似于以下内容
curl "https://domain/v1/example?q=(id:3)&fields=(author,title,year)" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
[
{
"author": "Stanimir Stoyanov",
"title": "OAuth2 with Phalcon",
"year": "2016"
}
]
curl "https://domain/v1/example?q=(year:2010)&fields=(author,title)" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
[
{
"author": "John Doe",
"title": "Greatest book"
},
{
"author": "John Doe",
"title": "Book of books"
}
]
可以通过包含查询参数' envelope=true '将信封包含在响应中。这将返回记录集和元信息作为正文。
curl "https://domain/v1/example?q=(year:2010)&fields=(author,title)" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"_meta": {
"status": "SUCCESS",
"count": 2
},
"records": [
{
"author": "John Doe",
"title": "Greatest book"
},
{
"author": "John Doe",
"title": "Book of books"
}
]
}
通常情况下,数据库字段名是snake_cased。然而,当与API一起工作时,开发人员通常更喜欢返回camelCase格式的JSON字段(许多API请求来自浏览器,在JS中)。此项目将默认将记录响应中的所有键从snake_case转换为camelCase。
可以通过设置JSONResponse的函数“convertSnakeCase(false)”来为API关闭此功能。
CSV
CSV是另一个实现的处理器。它使用第一条记录的键作为标题行,然后从数组中的每一行创建csv。可以关闭标题行以用于响应。
curl "https://domain/v1/example?q=(year:2010)&fields=(id,author,title)&type=csv" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
id,author,title
1,"John Doe","Greatest book"
2,"John Doe","Book of books"
错误
Phalcon2Rest\Exceptions\HttpException扩展PHP的本地异常。抛出此类异常将向客户端返回格式良好的JSON响应。
throw new \Phalcon2Rest\Exceptions\HttpException(
'Could not return results in specified format',
403,
null
array(
'dev' => 'Could not understand type specified by type paramter in query string.',
'internalCode' => 'NF1000',
'more' => 'Type may not be implemented. Choose either "csv" or "json"'
)
);
返回此内容
{
"devMessage": "Could not understand type specified by type paramter in query string.",
"error": 403,
"errorCode": "NF1000",
"more": "Type may not be implemented. Choose either \"csv\" or \"json\"",
"userMessage": "Could not return results in specified format"
}
示例控制器
示例控制器在/example设置了一个路由并实现了上述所有查询参数。您可以混合和匹配这些查询中的任何一项
api.example.local/v1/example?q=(author:Stanimir Stoyanov)
api.example.local/v1/example?fields=(id,title)
api.example.local/v1/example/1?fields=(author)&envelope=false
api.example.local/v1/example?type=csv
api.example.local/v1/example?q=(year:2010)&offset=1&limit=2&type=csv&fields=(id,author)
速率限制
已实现3个速率限制器,在config/config.ini
中配置
允许的访问令牌请求数量
[access_token_limits]
r1 = 5
此行设置每个IP对/access_token
和/authorize
端点的请求限制为每5秒1个。
有多少未授权请求?
未授权请求限制
r10 = 60
此行设置每个IP对其他请求的每1分钟10个请求限制,当缺少/无效的授权头时。OPTIONS请求也在此计数。
其他所有内容
通用请求限制
r600 = 3600
所有授权消费者每小时600个请求。用户和客户端在此单独计数。
跟踪速率限制器
每个请求都返回X-Rate-Limit-*头,例如。
HTTP/1.1 200 OK
X-Rate-Limit-Limit: 600
X-Rate-Limit-Remaining: 599
X-Rate-Limit-Reset: 3600
X-Record-Count: 2
X-Status: SUCCESS
E-Tag: 6385b20e0a8a3fb0edd588d630573f00
当达到限制时
< HTTP/1.1 429 Too Many Requests
< X-Rate-Limit-Limit: 600
< X-Rate-Limit-Remaining: 0
< X-Rate-Limit-Reset: 3355
< X-Status: ERROR
< E-Tag: f22e9815ad32e143287944e727627e9c
<
{
"errorCode": 429,
"userMessage": "Too Many Requests",
"devMessage": "You have reached your limit. Please try again after 3355 seconds.",
"more": "",
"applicationCode": "P1010"
}
CORS怎么办?
通过扩展RestController控制器,我们提供了基本的CORS策略。
将检查控制器,并将Access-Control-Allow-Methods填充为所有找到的有效方法。
curl -I -X OPTIONS https://domain/v1/authorize
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: origin, x-requested-with, content-type
curl -I -X OPTIONS https://domain/v1/example
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: origin, x-requested-with, content-type
请注意,没有安全的方法使用OPTIONS方法授权用户,因此这些请求被速率限制器计为未授权请求。
性能优化
默认使用FileCache,这非常慢。考虑使用memcached或redis。在项目中我们使用sqlite3作为数据库。考虑使用MySQL/PostgreSQL/MongoDB或其他具有预先缓存的数据库。
还有其他什么吗?
由于这是非常个人化的,因此尚未实现访问/刷新令牌的撤销。
查看Components\Oauth2\Repositories\AccessTokenRepository.php
和Components\Oauth2\Repositories\RefreshTokenRepository.php
每个访问都有一个唯一的标识符(jti),可用于撤销。
可以使用类似JWT.io的工具轻松调试令牌。