poweredlocal / vrata
使用 PHP7 和 Lumen 编写的 API 网关
Requires
- php: >=7.1.3
- appzcoder/lumen-routes-list: ^1.0
- barryvdh/laravel-cors: ^0.11
- doctrine/dbal: ~2.3
- dusterio/lumen-passport: ^0.2
- guzzlehttp/guzzle: ~6.0
- laravel/lumen-framework: 5.6.*
- league/flysystem: ^1.0
- vlucas/phpdotenv: ~2.2
- webpatser/laravel-uuid: 2.*
Requires (Dev)
- codeclimate/php-test-reporter: dev-master
- filp/whoops: ~2.0
- fzaninotto/faker: ~1.4
- phpunit/phpunit: ~6.0
README
使用 PHP 和 Lumen 实现的 API 网关。目前仅支持 JSON 格式。
序言
API 网关是微服务架构模式中的重要组成部分 - 它位于所有服务之前的一层。了解更多
概述
Vrata(俄语意为“大门”)是一个使用 PHP7 和 Lumen 框架实现的简单 API 网关
要求与依赖
- PHP >= 7.0
- Lumen 5.3
- Guzzle 6
- Laravel Passport(包含 Lumen Passport)
- Memcached(用于请求节流)
以 Docker 容器运行
理想情况下,您希望将其作为无状态的 Docker 容器运行,完全通过环境变量进行配置。因此,您甚至不需要自己部署此代码 - 只需使用我们的 公共 Docker Hub 镜像。
部署它就像
$ docker run -d -e GATEWAY_SERVICES=... -e GATEWAY_GLOBAL=... -e GATEWAY_ROUTES=... pwred/vrata
其中环境变量是 JSON 编码的设置(请参阅下面的配置选项)。
通过环境变量进行配置
理想情况下,您根本不需要触摸任何代码。您只需获取最新的 Docker 镜像,设置环境变量即可。API 网关不是存储任何业务逻辑的地方,API 网关是一个智能代理,可以发现微服务,查询它们并对其响应进行最小调整。
术语和结构
典型 API 网关的内部结构 - 微服务设置如下
由于 API 网关实际上没有状态,因此它非常适合水平扩展。
Lumen 变量
CACHE_DRIVER
建议将此设置为 'memcached' 或 Lumen 支持的其他共享缓存。如果您正在运行多个 API 网关实例,API 速率限制依赖于缓存。
DB_DATABASE, DB_HOST, DB_PASSWORD, DB_USERNAME, DB_CONNECTION
您的数据库凭据的标准 Lumen 变量。如果您在数据库中保留用户,请使用它们。有关支持的数据库名单,请参阅 Laravel/Lumen 文档。
APP_KEY
Lumen 应用程序密钥
网关变量
PRIVATE_KEY
将您的私有 RSA 密钥放在此变量中
您可以使用 OpenSSL 生成密钥
$ openssl genrsa -out private.key 4096
将换行符替换为 \n
awk 1 ORS='\\n' private.key
PUBLIC_KEY
将您的公共 RSA 密钥放在此变量中
使用 OpenSSL 提取公钥
$ openssl rsa -in private.key -pubout > public.key
将换行符替换为 \n
awk 1 ORS='\\n' public.key
GATEWAY_SERVICES
API 网关后面微服务的 JSON 数组
GATEWAY_ROUTES
包含任何聚合路由的额外路由的 JSON 数组
GATEWAY_GLOBAL
带有全局设置的 JSON 对象
日志记录
目前仅支持 LogEntries。要将 nginx 和 Lumen 日志发送到 LE,只需设置两个环境变量
LOGGING_ID
此应用程序的标识字符串
LOGGING_LOGENTRIES
您的 LogEntries 用户密钥
特性
- 内置 OAuth2 服务器以处理所有传入请求的认证
- 聚合查询(组合 2+ 个 API 的输出)
- 输出重构
- 聚合Swagger文档(合并底层服务的Swagger文档)*
- 基于Swagger JSON的自动路由挂载
- 同步和异步出站请求
- DNS服务发现
安装
您可以选择git clone或者使用composer(Packagist)。
$ composer create-project poweredlocal/vrata
特性
自动导入符合Swagger规范的端点
您可以定义Swagger文档端点的URL - 默认URL以及必要时按服务定制的URL。假设您有一个在/api/doc
上运行的Symfony2微服务,并且使用Nelmio ApiDoc插件。您的微服务返回如下:
$ curl -v http://localhost:8000/api/doc { "swaggerVersion": "1.2", "apis": [{ "path": "\/uploads", "description": "Operations on file uploads." }], "apiVersion": "0.1", "info": { "title": "Symfony2", "description": "My awesome Symfony2 app!" }, "authorizations": [] } $ curl -v http://localhost:8000/api/doc/uploads { "swaggerVersion": "1.2", "apiVersion": "0.1", "basePath": "\/api", "resourcePath": "\/uploads", "apis": [{ "path": "\/uploads", "operations": [{ "method": "GET", "summary": "Retrieve list of files", "nickname": "get_uploads", "parameters": [], "responseMessages": [{ "code": 200, "message": "Returned when successful", "responseModel": "AppBundle.Entity.Upload[items]" }, { "code": 500, "message": "Authorization error or any other problem" }], "type": "AppBundle.Entity.Upload[items]" } }, "produces": [], "consumes": [], "authorizations": [] }
此端点可以在容器启动时(或您认为合适的时候)自动导入到API网关。
假设此微服务已列在GATEWAY_SERVICES中,我们现在可以运行自动导入。
$ php artisan gateway:parse ** Parsing service1 Processing API action: http://localhost:8000/uploads Dumping route data to JSON file Finished!
这就完成了 - Vrata现在将“代理”所有对/uploads
的请求到这个微服务。
OAuth2认证
Vrata附带Laravel Passport - 一个功能齐全的OAuth2服务器。JSON Web Tokens用于认证所有API请求,目前仅支持本地持久化(数据库)。但是,将OAuth2服务器移至外部并依赖公钥进行JWT令牌验证是微不足道的。
如果传入的承载令牌无效,Vrata将返回401未经授权错误。如果令牌有效,Vrata在调用底层微服务时会添加两个额外头信息
X-User
从JSON Web Token中提取的数字主题ID。您的微服务可以始终假设认证部分已完成并信任此用户ID。如果您想实现授权,您可能基于此ID或令牌作用域(见下文)。
X-Token-Scopes
从JSON web token中提取的令牌作用域。逗号分隔(例如:read,write
)
您的微服务可以使用这些用于授权目的(限制某些操作等)。
X-Client-Ip
原始用户IP地址。
基本输出突变
您可以使用操作的output
属性进行基本的JSON输出突变。例如。
[ 'service' => 'service1', 'method' => 'GET', 'path' => '/pages/{page}', 'sequence' => 0, 'output_key' => 'data' ];
service1的响应将包含在最终的输出中的data键下。
output_key
可以是一个数组,以便进行进一步的突变
[ 'service' => 'service1', 'method' => 'GET', 'path' => '/pages/{page}', 'sequence' => 0, 'output_key' => [ 'id' => 'service_id', 'title' => 'service_title', '*' => 'service_more' ] ];
这将把id属性的内容分配给garbage_id,title分配给service_title,其余的内容将包含在输出JSON的service_more属性中。
性能
性能是API网关的关键指标之一,这就是我们选择Lumen的原因——在基本机器上启动只需要~25ms。
请看一个聚合请求的示例。首先,让我们对底层微服务进行单独的请求
$ time curl http://service1.local/devices/5 {"id":5,"network_id":2,...} real 0m0.025s $ time curl http://service1.local/networks/2 {"id":2,...} real 0m0.025s $ time curl http://service2.local/visits/2 [{"id":1,...},{...}] real 0m0.041s
所以这是91ms的真实操作系统时间 - 包括所有与Web服务器相关的开销。现在让我们向API网关发出一个单一的聚合请求,在幕后它将执行相同的3个请求
$ time curl http://gateway.local/devices/5/details {"data":{"device":{...},"network":{"settings":{...},"visits":[]}}} real 0m0.056s
所有3个请求只需要56ms!第二个和第三个请求是并行执行的(异步模式)。
这相当不错,我们认为!
示例
示例1:单个简单微服务
让我们假设我们有一个非常简单的设置:API网关 + 后面跟的一个微服务。
首先,我们需要让网关了解此微服务,方法是将它添加到GATEWAY_SERVICES环境变量中。
{
"service": []
}
其中service是我们为微服务选择的昵称。数组为空,因为我们将依赖默认设置。我们的服务有一个有效的Swagger文档端点,运行在api/doc
URL上。
接下来,我们在GATEWAY_GLOBAL环境变量中提供全局设置
{ "prefix": "/v1", "timeout": 3.0, "doc_point": "/api/doc", "domain": "supercompany.io" }
这告诉网关,未提供显式URL的服务将在{serviceId}.{domain}进行通信,因此我们的服务将通过service.supercompany.io进行接触,请求超时为3秒,Swagger文档将从/api/doc
加载,所有路由都将使用“v1”作为前缀。
但是,我们可以使用GATEWAY_SERVICES数组中的“hostname”键显式指定服务的域名。
现在我们可以运行php artisan gateway:parse
来强制Vrata解析该服务提供的Swagger文档。所有记录的路由都将在这个API网关中公开。
如果您使用我们的Docker镜像,每次启动容器时都会执行此命令。
现在,如果您的服务有一个路由GET http://service.supercompany.io/users
,它将作为GET http://api-gateway.supercompany.io/v1/users
提供,并且所有请求都将接受JSON Web Token检查和速率限制。
别忘了设置PRIVATE_KEY和PUBLIC_KEY环境变量,它们对于认证工作至关重要。
示例2:多个微服务与聚合请求
这一次,我们将在API网关后面添加两个服务 - 一个使用Swagger 1文档,另一个使用Swagger 2文档。Vrata会自动检测Swagger版本,所以我们不需要在别处指定。让我们首先定义GATEWAY_SERVICES
环境变量
{ "core": [], "service1": [] }
因此,我们有“core”和“service1”两个服务,Vrata会假设DNS主机名将与这些匹配。
现在让我们定义GATEWAY_GLOBAL
变量 - 这个变量包含API网关的全局设置
{ "prefix": "/v1", "timeout": 10.0, "doc_point": "/api/doc", "domain": "live.vrata.io" }
由于第一个设置,从Swagger导入的所有路由都将使用"/v1"作为前缀。10秒是我们给API网关用于其后面微服务的内部请求的超时时间。"doc_point"是Swagger文档的URI,而"domain"是将被添加到每个服务名称的DNS域。
因此,当Vrata尝试加载"core"服务的Swagger文档时,它将访问http://core.live.vrata.io/api/doc URL。如果您为每个微服务有唯一的Swagger URI,您可以为每个服务单独定义"doc_point"。
设置这两个变量就足以开始使用Vrata - 它将导入"core"和"service1"的所有路由,并开始代理对它们的请求。
但是,如果我们需要更复杂的功能 - 例如,涉及多个微服务的聚合请求,我们需要定义第三个环境变量 - GATEWAY_ROUTES
。
考虑以下示例
[{ "aggregate": true, "method": "GET", "path": "/v1/connections/{id}", "actions": { "venue": { "service": "core", "method": "GET", "path": "venues/{id}", "sequence": 0, "critical": true, "output_key": "venue" }, "connections": { "service": "service1", "method": "GET", "path": "connections/{venue%data.id}", "sequence": 1, "critical": false, "output_key": { "data": "venue.clients" } }, "access-lists": { "service": "service1", "method": "GET", "path": "/metadata/{venue%data.id}", "sequence": 1, "critical": false, "output_key": { "data": "venue.metadata" } } } }, { "method": "GET", "path": "/v1/about", "public": true, "actions": [{ "service": "service1", "method": "GET", "path": "static/about", "sequence": 0, "critical": true }] }, { "method": "GET", "path": "/v1/history", "raw": true, "actions": [{ "method": "GET", "service": "core", "path": "/connections/history" }] }]
上面的配置定义了3个路由 - 两个带有自定义设置的常规请求和一个聚合请求。让我们从简单的请求开始
{ "method": "GET", "path": "/v1/about", "public": true, "actions": [{ "service": "service1", "method": "GET", "path": "static/about", "sequence": 0, "critical": true }] }
这个定义将为API网关添加一个"/v1/about"路由,它将是公开的 - 完全不需要访问令牌,认证将被绕过。它将代理到http://service1.live.vrata.io/static/about并返回任何返回的内容。
另一个简单的路由
{ "method": "GET", "path": "/v1/history", "raw": true, "actions": [{ "method": "GET", "service": "core", "path": "/connections/history" }] }
这将添加一个"/v1/history"端点,它将从http://core.live.vrata.io/connections/history请求数据。注意"raw"标志 - 这意味着Vrata根本不会进行任何JSON解析(因此您无法像结果那样修改输出)。这对性能很重要 - 如果您使用json_decode()和json_encode()对一个大字符串进行解码和编码,PHP可能会崩溃
- 数组和对象在PHP中非常占用内存。
最后是我们的聚合路由
{ "aggregate": true, "method": "GET", "path": "/v1/connections/{id}", "actions": { "venue": { "service": "core", "method": "GET", "path": "venues/{id}", "sequence": 0, "critical": true, "output_key": "venue" }, "connections": { "service": "service1", "method": "GET", "path": "connections/{venue%data.id}", "sequence": 1, "critical": false, "output_key": { "data": "venue.clients" } }, "access-lists": { "service": "service1", "method": "GET", "path": "/metadata/{venue%data.id}", "sequence": 1, "critical": false, "output_key": { "data": "venue.metadata" } } } }
第一个属性将其标记为聚合路由 - 这很容易理解。路由将挂载为"/v1/connections/{id}",其中"id"可以是任何字符串或数字。然后,此路由涉及对微服务的3次请求,其中两个可以并行进行 - 因为它们具有相同的序列号1。
首先,Vrata 将向 http://core.live.vrata.io/venues/{id} 发送请求,其中 {id} 是请求中的参数。此路由操作被标记为关键 - 因此,如果失败,整个请求将被放弃。此操作的所有输出都将在最终的 JSON 输出中以 "venue" 属性的形式展示。
然后,将同时启动两个请求 - 一个是向 http://service1.live.vrata.io/connections/{id} 发送,另一个是向 http://service1.live.vrata.io/metadata/{id} 发送。这次,{id} 来自上一个操作的输出。Vrata 将收集所有请求的输出并将其提供给所有后续请求。
由于这两个请求总是在第一个请求之后发生(因为顺序设置),它们可以访问其输出。注意路径中的 {venue%data.id} - 这指的是 "venue"(上一个操作的名称)和 "data" 对象的 "id" 属性(点表示法中的 "data.id")。
这两个操作均设置为非关键操作 - 如果失败,用户仍然会收到响应,但相应的字段将为空。
我们只从两个响应中提取 "data" JSON 属性,并将其注入到最终的响应中作为 "venue.clients" 和 "venue.metadata"。
示例 3:多个微服务与聚合 POST/PUT/DELETE 请求
您的 POST、PUT 或 DELETE 请求的初始正文包含在您的 json 中可用的源标签。您可以为每个请求使用可选的正文参数。您可以使用源标签使用初始请求中发送的正文。您还可以在正文参数中像在 GET 聚合请求中一样使用每个操作的响应。
{ "aggregate": true, "method": "PUT", "path": "/v1/unregister/sendaccess", "actions": { "contact": { "service": "contact", "method": "PUT", "path": "unregister/newsletter", "sequence": 0, "critical": true, "body": { "email": "{origin%email}" }, "output_key": "register" }, "template": { "service": "notice", "method": "POST", "path": "notice/email/generate", "sequence": 1, "critical": true, "body": { "validationToken": "{contact%validationToken}", "noticeTypeId": "{contact%validationType}" }, "output_key": "notice" }, "check": { "service": "email", "method": "POST", "path": "email/send", "sequence": 2, "critical": true, "body": { "to": "{origin%email}", "sujet": "Email object", "queue_name": "urgent", "message_html": "{template%htmlTemplate}", "message_text": "text version", "from": "support@example.org", "nom": "Sender name" }, "output_key": "result" } } }
许可证
MIT 许可证 (MIT)
版权所有 (c) 2017-2018 PoweredLocal
特此授予任何获得本软件及其相关文档副本(以下简称“软件”)的人免费使用软件的权利,不受任何限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向获得软件的人提供软件副本,但需遵守以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或实质性部分中。
本软件按“原样”提供,不提供任何形式的保证,明示或暗示,包括但不限于适销性、针对特定目的的适用性和非侵权性保证。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论此类责任是基于合同、侵权或其他方式,是否因软件或软件的使用或其他方式而产生。