使用Kong来控制对API的访问 – API密钥和ACL版
首先
我們將使用開源API網關Kong來介紹跨多篇文章討論API存取控制的方法。
在Kong中,可以通过插件形式为创建的API添加各种功能(如日志记录、流量控制等)。我们要介绍的访问控制机制也是以插件形式提供的,根据Plugins页面上的介绍,可以对API应用各种访问控制(OpenID Connect RP和OAuth2.0 Introspection等某些插件仅提供收费版(企业版))。
这样一来,以前在每个资源上都要实现访问控制(认证和授权)、流量控制、日志记录等功能,现在可以通过API网关(Kong)进行统一管理(不需要在每个资源上实现OAuth2和流量控制等机制)。Kong的官方网站上也介绍了这些优点。
本文介绍了一种在Kong中进行访问控制实践的第一步,即结合Key认证插件和ACL插件对API实施访问控制的方法。
这个配置在本文中实现。
此外,当调用API时,我们决定使用httpbin.org/anything作为资源服务器来返回请求信息(这个网站非常方便,可以用来确认Kong发送了什么样的请求~等等)。
安装Kong
如果您已经安装了 Kong,请跳过本节。
由于有许多关于Kong的安装文章,所以我想要省略一些,并介绍最近发布的0.12.x版本(截至2018年2月19日)。环境假设为Docker,请参考官方文档(Docker安装),基本上只需要复制并执行以下3个命令即可。
准备保存Kong设置的数据库
$ docker run -d --name kong-database \
-p 5432:5432 \
-e "POSTGRES_USER=kong" \
-e "POSTGRES_DB=kong" \
postgres:9.4
② DB与Kong的合作(迁移)
$ docker run --rm \
--link kong-database:kong-database \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
kong:latest kong migrations up
③ K起动
$ docker run -d --name kong \
--link kong-database:kong-database \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
-e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
-e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
-e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
-e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
-e "KONG_ADMIN_LISTEN_SSL=0.0.0.0:8444" \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
kong:latest
关于③,官方文档中指定了Docker启动时的选项,包括访问日志的输出目标和管理员端口的指定,但如果想要设置其他配置项,请参考配置参考文档。
Kong的操作测试
如果按照①~③的步骤进行,并且成功启动了Kong,那么当执行以下curl命令时,会返回表示Kong基本信息的JSON数据。
$ curl http://localhost:8001/
{"version":"0.12.1","plugins":{"enabled_in_cluster":[], ~以下省略~ }
顺便提一下,就像下表所示,在Kong中我们使用8001端口来创建、配置API以及应用插件,同时使用8000端口来调用已创建的API。
既然准备妥当,我们首先要创建Kong API。
创建简洁的API
在给API添加访问控制功能之前,首先我们需要创建一个基础API。在这个阶段,我们只是简单地创建API,所以任何人都可以调用所创建的API。
可以通过向http://[主机]:8001/apis发送POST请求来创建新的API。在请求中指定API名称和API的访问URL。具体来说,按照以下格式发送请求:
$ curl -i -X POST \
--url http://[ホスト]:8001/apis/ \
--data 'name=[API名]' \
--data 'uris=/[APIのURI]' \
--data 'upstream_url=[APIアクセス時に呼び出すサービスのURI]'
上述请求中的data部分具有以下表格中的含义。
http://[ホスト]:8000/[uris]
を指定してこのAPIをコールします。upstream_urlこのAPIをコールした際に呼び出されるURL。(リソースサーバに相当します)让我们立即发送一个创建API的请求。这次我们将创建一个名为sandbox-api的API。uris将与API名称相同,并设置为/sandbox-api。此外,将upstream_url设置为http://httpbin.org/anything。如果处理成功,将返回状态代码201和包含已创建API的JSON。
$ curl -i -X POST \
--url http://localhost:8001/apis/ \
--data 'name=sandbox-api' \
--data 'uris=/sandbox-api' \
--data 'upstream_url=http://httpbin.org/anything'
HTTP/1.1 201 Created
Date: Mon, 19 Feb 2018 09:32:32 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.12.1
{"created_at":1519032752107,"strip_uri":true,"id":"287fa349-eb7f-456a-948c-0270b0fc4511","name":"sandbox-api","http_if_terminated":false,"preserve_host":false,"upstream_url":"http:\/\/httpbin.org\/anything","uris":["\/sandbox-api"],"upstream_connect_timeout":60000,"upstream_send_timeout":60000,"upstream_read_timeout":60000,"retries":5,"https_only":false}
由于成功创建了API,接下来我将调用所创建的API以进行确认。由于本次测试与Kong环境相同,我会将主机指定为localhost,并在主机后面指定API创建时的URIs以进行调用。
$ curl -i localhost:8000/sandbox-api
如果API调用正常,您将能够获取设置为upstream_url的资源,并伴随着200状态。以下JSON具有X-Kong-Proxy-Latency,并来自资源服务器(httpbin.org/anything)的响应。(顺便提一下,您也可以确认Kong在请求头中添加了信息,例如X-Kong-Upstream-Latency)
$ curl -i localhost:8000/sandbox-api
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 351
Connection: keep-alive
Server: meinheld/0.6.1
Date: Mon, 19 Feb 2018 09:41:45 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0
Via: kong/0.12.1
X-Kong-Upstream-Latency: 30
X-Kong-Proxy-Latency: 0
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Forwarded-Host": "localhost"
},
"json": null,
"method": "GET",
"origin": [mask],
"url": "http://localhost/anything"
}
好的,现在我们已经完成了API的创建和操作确认。
然而,目前情况下,对于API并没有进行任何访问控制,因此任何人都可以调用该API,就像下图所示的状态一样。
首先,我们将通过对这个API应用各种插件来实现应用访问控制的API。
让我们先应用密钥认证插件。
应用Key Authentication插件
为了实现只有特定用户能访问API的配置,需要同时应用Key Authentication插件和ACL插件,但首先我们将介绍Key Authentication插件。通过引入这个插件,我们可以将对API的访问限定在拥有正确密钥的用户之中。换句话说,这个插件可以拒绝未被授权的请求。
以下是使用密钥进行操作的图示。图中的AliceSecretKey、BobSecretKey和CharlieSecretKey分别对应着密钥。
安装Key Authentication插件需要以下3个步骤:
-
- ① sandbox-apiにKey Authenticationプラグインを適用する
-
- ② コンシューマ(ユーザ)を作成する
- ③ コンシューマを識別するキーを設定する
那么,我们将逐步解释设置方法。
应用Key Authentication插件到API
如果要对API应用插件,请将插件名称和插件配置POST到http://[host]:8001/apis/[API名称(name)]/plugins。如果要应用Key Authentication插件,请按照以下格式POST设置。
$ curl -X POST localhost:8001/apis/[API名]/plugins \
--data "name=key-auth" \
--data "config.key_names=[キーを指定する際の属性名]"
key-auth
を指定します。この後設定するACLプラグインではacl
を指定します。config.key_namesKey Authenticationプラグインでは、クエリ文字列でユーザを識別します。config.key_namesはクエリ文字列の左側(属性名)を指定します。本次操作是对先前创建的sandbox-api应用Key认证插件进行应用,在config.key_names中设定了sandboxApiKey。发送此POST请求并成功处理后,将返回一个包含插件各项配置的JSON作为响应。
$ curl -X POST localhost:8001/apis/sandbox-api/plugins \
--data "name=key-auth" \
--data "config.key_names=sandboxApiKey"
{"created_at":1519125441000,"config":{"key_names":["sandboxApiKey"],"key_in_body":false,"anonymous":"","run_on_preflight":true,"hide_credentials":false},"id":"3a342b9e-bf22-4b28-b3f2-ebc0ea54b0aa","name":"key-auth","api_id":"c9c6c0bb-445c-491e-9307-0157fe5d4e74","enabled":true}
现在,我们可以将Key认证插件应用于sandbox-api。在此阶段,我们可以在API末尾添加查询字符串并调用http://localhost:8000/sandbox-api?sandboxApiKey=[用户的Key]。
然而,目前还没有进行查询字符串参数的设置(尚未创建用户的账户)。因此,让我们在步骤②中继续创建一个相应的用户。
② 创建一个消费者(用户)
在这里,出现了一个新概念,那就是消费者。关于消费者,根据官方文件的描述如下。
消費者对象代表API的消费者或用户。
作為文件的意思是指API的消費者(使用者)。從其含意來看,它似乎定義使用者的集合(組織)為消費者。順便提一下,消費者是OAuth2插件等概念中常被使用的,所以如果考慮使用OAuth2插件,強烈建議先閱讀官方文件。
本次活动将以消费者作为用户,创建Alice消费者、Bob消费者和Charlie消费者。根据以下格式进行创建。
$ curl -X POST localhost:8001/consumers/ \
--data "username=<USERNAME>" \
--data "custom_id=<CUSTOM_ID>"
如果要创建一个名为Alice的消费者,您可以使用以下请求。
$ curl -X POST localhost:8001/consumers/ \
--data "username=alice" \
--data "custom_id=alice"
{"custom_id":"alice","created_at":1519125666000,"username":"alice","id":"7265a18a-3b2b-4a6a-a51d-18e6e6dea2c7"}
以同样的方式,还需要创建Bob和Charlie两个消费者。
$ curl -X POST localhost:8001/consumers/ \
--data "username=bob" \
--data "custom_id=bob"
{"custom_id":"bob","created_at":1519127073000,"username":"bob","id":"b4835592-b912-41a8-bfa2-88416db69de5"}
$ curl -X POST localhost:8001/consumers/ \
--data "username=charlie" \
--data "custom_id=charlie"
{"custom_id":"charlie","created_at":1519127112000,"username":"charlie","id":"49509fc9-d6a0-4d6e-887f-73d95a6d83dd"}
设定识别消费者的键值
如果能够创建消费者,则接下来需要为所创建的消费者设置一个键。在这里设置的键对应于之前设定的查询字符串参数部分([用户的键]部分)。
按照下面的格式为消费者设置键值。
$ curl -X POST localhost:8001/consumers/[コンシューマ名]/key-auth \
--data "key=[コンシューマのKey]"
这次,我们将把字符串”AliceSecretKey”作为Alice消费者的密钥进行设置。
$ curl -X POST localhost:8001/consumers/alice/key-auth \
--data "key=AliceSecretKey"
{"id":"fd26bf85-b73e-4eaa-b1bd-8c2b3dfe2d2b","created_at":1519125820000,"key":"AliceSecretKey","consumer_id":"7265a18a-3b2b-4a6a-a51d-18e6e6dea2c7"}
同样地,为Bob消费者设置”BobSecretKey”,为Charlie消费者设置”CharlieSecretKey”。
$ curl -X POST localhost:8001/consumers/bob/key-auth \
--data "key=BobSecretKey"
{"id":"e504aada-25a3-4e59-a9a5-21e8121a0677","created_at":1519127653000,"key":"BobSecretKey","consumer_id":"b4835592-b912-41a8-bfa2-88416db69de5"}
$ curl -X POST localhost:8001/consumers/charlie/key-auth \
--data "key=CharlieSecretKey"
{"id":"51d48a78-a3e8-489b-9cc5-2108b69ce190","created_at":1519127695000,"key":"CharlieSecretKey","consumer_id":"49509fc9-d6a0-4d6e-887f-73d95a6d83dd"}
在这个步骤中,我们已经成功将Key Authentication插件应用于纯净的API,并完成了设置Key Authentication所需的查询字符串参数(创建Consumer并设置密钥)。
让我们立即在查询字符串中添加参数并访问API。如果能正确附加消费者密钥,应该能够成功获取内容。
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=AliceSecretKey
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 558
Connection: keep-alive
Server: meinheld/0.6.1
Date: Tue, 20 Feb 2018 11:26:50 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0
Via: kong/0.12.1
X-Kong-Upstream-Latency: 28
X-Kong-Proxy-Latency: 0
{
"args": {
"sandboxApiKey": "AliceSecretKey"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Consumer-Custom-Id": "alice",
"X-Consumer-Id": "7265a18a-3b2b-4a6a-a51d-18e6e6dea2c7",
"X-Consumer-Username": "alice",
"X-Forwarded-Host": "localhost"
},
"json": null,
"method": "GET",
"origin": "172.17.0.1, 18.218.119.187",
"url": "http://localhost/anything?sandboxApiKey=AliceSecretKey"
}
如果提供了无效的键或者根本没有提供查询字符串,将会返回以下错误。
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=MichelSecretKey
HTTP/1.1 403 Forbidden
Date: Tue, 20 Feb 2018 11:29:16 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.12.1
{"message":"Invalid authentication credentials"}
$ curl -i http://localhost:8000/sandbox-api
HTTP/1.1 401 Unauthorized
Date: Tue, 20 Feb 2018 11:29:23 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
WWW-Authenticate: Key realm="kong"
Server: kong/0.12.1
{"message":"No API key found in request"}
这样一来,除了消费者之外,就无法访问API了。
然而,在Kong上,只要是存在的消费者,任何人都可以访问API。实际情况是,很少有“只要是消费者就可以访问”的情况,我认为大多数情况下,我们希望对每个消费者单独进行访问控制。
只需一种选择:应用下面要解释的ACL插件,您就可以在消费者层面上控制访问,所以让我们立即进行设置吧。
应用ACL插件
使用Key Authentication插件,并应用ACL插件,可以在每个消费者单位中控制对API的访问。
具体而言,可以通过为每个消费者设置”ACL组”,并通过ACL插件指定允许或拒绝访问的ACL组来实现访问控制。可以在ACL插件的白名单和黑名单功能中设置允许或拒绝访问的ACL组。
要安装ACL插件,请按照以下2个步骤进行操作。
-
- ① コンシューマにACLグループを設定する
- ② ACLプラグインを適用し、アクセスを許可・許可しないACLグループを設定する
现在,我将逐步为您解释设置方法。
给消费者设置ACL组。
将ACL组作为访问控制的单位设置给消费者。格式如下所示。
$ curl -X POST http://[ホスト]:8001/consumers/[コンシューマ名]/acls \
--data "group=[ACLグループ名]"
这次,ACL组名已经分配给了各个消费者,具体如下所示。由于alice和bob都希望允许访问,所以给他们分配了相同的ACL组。
现在,我们要为每个用户设置ACL组。
$ curl -X POST http://localhost:8001/consumers/alice/acls \
--data "group=allow-group"
{"group":"allow-group","created_at":1519273998000,"id":"9f04008f-9914-420d-a3a2-edabf32d37ac","consumer_id":"82a61b1a-97b6-43a4-82ed-97040b108707"}
$ curl -X POST http://localhost:8001/consumers/bob/acls \
--data "group=allow-group"
{"group":"allow-group","created_at":1519274058000,"id":"1c048e7a-99ab-4328-a431-a63e43a26aa3","consumer_id":"f6255716-6808-4229-90ce-d8b86c793b8a"}
$ curl -X POST http://localhost:8001/consumers/charlie/acls \
--data "group=deny-group"
{"group":"deny-group","created_at":1519274108000,"id":"0f820d10-9e94-495e-9637-c232bb058a79","consumer_id":"f8158211-be7a-47e9-8cc6-b9d4d1630d61"}
ACL组的设置已经完成。接下来,将进行允许或拒绝对创建的ACL组的访问的设置。
应用ACL插件,并设置允许或不允许访问的ACL组。
我们将对sandbox-api应用ACL插件进行配置,但在进行应用时,需要设置config.whitelist或config.blacklist。
config.whitelist的含义是将指定的ACL组注册为白名单配置。由于是白名单,只允许指定ACL组访问,而阻止其他ACL组的访问。config.blacklist则是黑名单配置,与config.whitelist相反(拒绝指定ACL组的访问,允许其他ACL组的访问)。
设定的格式如下所示。
$ curl -X POST http://[ホスト]:8001/apis/[API名]/plugins \
--data "name=acl" \
--data "config.whitelist=[ACLグループ]"
我希望该次为alice消费者和bob消费者所属的ACL组(allow-group)允许访问,并且拒绝charlie消费者的ACL组(deny-group)访问,因此要在config.whitelist中指定allow-group。
$ curl -X POST http://localhost:8001/apis/sandbox-api/plugins \
--data "name=acl" \
--data "config.whitelist=allow-group"
{"created_at":1519276536000,"config":{"whitelist":["allow-group"]},"id":"79a8ee65-3290-4da8-a6e2-2113928503e1","name":"acl","api_id":"cdab454f-75ff-4761-9eb9-4aee1b33b262","enabled":true}
我们已经完成了所有的设置。让我们来测试一下看看。
Alice和Bob消费者使用API进行访问。
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=AliceSecretKey
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 558
Connection: keep-alive
Server: meinheld/0.6.1
Date: Thu, 22 Feb 2018 05:11:53 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0
Via: kong/0.12.1
X-Kong-Upstream-Latency: 29
X-Kong-Proxy-Latency: 19
{
"args": {
"sandboxApiKey": "AliceSecretKey"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Consumer-Custom-Id": "alice",
"X-Consumer-Id": "82a61b1a-97b6-43a4-82ed-97040b108707",
"X-Consumer-Username": "alice",
"X-Forwarded-Host": "localhost"
},
"json": null,
"method": "GET",
"origin": "172.17.0.1, 18.218.119.187",
"url": "http://localhost/anything?sandboxApiKey=AliceSecretKey"
}
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=BobSecretKey
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 550
Connection: keep-alive
Server: meinheld/0.6.1
Date: Thu, 22 Feb 2018 05:12:48 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0
Via: kong/0.12.1
X-Kong-Upstream-Latency: 28
X-Kong-Proxy-Latency: 15
{
"args": {
"sandboxApiKey": "BobSecretKey"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Consumer-Custom-Id": "bob",
"X-Consumer-Id": "f6255716-6808-4229-90ce-d8b86c793b8a",
"X-Consumer-Username": "bob",
"X-Forwarded-Host": "localhost"
},
"json": null,
"method": "GET",
"origin": "172.17.0.1, 18.218.119.187",
"url": "http://localhost/anything?sandboxApiKey=BobSecretKey"
}
您已成功访问了无事API,接下来我们将使用Charlie消费者来访问该API。
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=CharlieSecretKey
HTTP/1.1 403 Forbidden
Date: Thu, 22 Feb 2018 05:16:22 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.12.1
{"message":"You cannot consume this service"}
这里也按照预期被拒绝访问。当然,如果指定了不存在的密钥或者未提供密钥,访问也会被拒绝。
$ curl -i http://localhost:8000/sandbox-api?sandboxApiKey=MichelSecretKey
HTTP/1.1 403 Forbidden
Date: Thu, 22 Feb 2018 05:18:52 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.12.1
{"message":"Invalid authentication credentials"}
$ curl -i http://localhost:8000/sandbox-api
HTTP/1.1 401 Unauthorized
Date: Thu, 22 Feb 2018 05:20:42 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
WWW-Authenticate: Key realm="kong"
Server: kong/0.12.1
{"message":"No API key found in request"}
以此告一段落,我们成功实现了最初构想的结构!
最后
这次我们使用了密钥(Key)和访问控制列表(ACL),为API增加了访问控制功能。但是,在实际的使用场景中,随着用户数量的增加,仅仅通过Kong来管理消费者创建和密钥发放变得困难,同时也有一些需求希望通过已存在的Keycloak2等IdP(身份提供者)来进行用户认证。
如果有机会的话,我想尝试将用户认证部分放置在Kong外,并利用Kong的OAuth2.0认证插件,根据外部认证信息验证API的访问权限。
此外,在我们的OpenStandia中,我们为各种开源软件提供最新资讯和技术支持。如果您有兴趣,请务必查看!
感谢您一直以来的观看。
通过在config.whitelist中指定allow-group,我们实现了访问控制。但是,通过在config.blacklist中指定deny-group也可以实现类似的访问控制。
如果您对Keycloak感兴趣,请务必查看Keycloak by OpenStandia Advent Calendar 2017!它包含了各种内容,从Keycloak的安装到应用程序的使用。