使用Keycloak在API网关中限制对API的访问

今天要做的事情

在第19天的圣诞降临活动中,将介绍如何使用API Gateway进行访问限制(授权),并讨论保护手机应用程序的API服务器的方法。由于希望在一天内完成,因此文章变得相当冗长。

:warning: AWS API Gatewayの話は全く出てきません。”AWS”でググって来てしまった人は、申し訳ないですが、ブラウザバックをお願いします。

API Gateway是什么?

假设你要开发并发布一个Web服务。你可以选择传统的JSP或(没有x的)ASP等老式Web应用程序,但现代化的服务只需要提供REST API。那么,这个API是任何人都能使用的吗?显然不是,使用者会有限制……看起来需要身份验证。然而,通常API不是所有经过身份验证的人都能平等使用,一般的做法是○○API是任何人可用,但××API只能特定的人使用。换句话说,需要有访问限制(授权)。

虽然我可以根据我的方式进行审批,但如果可以的话,遵循常用的协议才符合情理。常见的协议应该是 OAuth2.0。

如果是OAuth2.0,可能根本不需要自己開發,網上可以搜尋到很多OAuth2.0的模塊和函式庫。但從API的角度來看,這些模塊和函式庫中需要獲取的信息有哪些呢?只有用戶信息(≒ID令牌)。如果只需要這些信息,真的需要將它們作為Web容器上的模塊嗎?如果在請求之前需要通過授權進行訪問限制,難道反向代理服務器就不行嗎?

API Gateway是一个反向代理服务器,它聪明地处理访问限制。实际上,它不仅限制访问,还有流量限制、负载平衡、审计日志输出以及类似Apache中的虚拟主机功能等等。它将这些功能集中可视化,作为一个单独的独立服务提供,同时还将其与Web应用程序分离(解耦),作为一种”授权服务”。傲慢一点的说法是,在Web服务层之前的授权层,或者说”认可层”。

API Gateway的一般配置

API网关的核心是一个反向代理服务器,所以它被放置在API服务器之前。另外,用户(或者使用API的客户端)通常将API网关放置在DMZ中,而API服务器则在其后面。

2017-11-22_105314.png

在这里最重要的点是不是API服务器,而是”应用程序(客户端)”的部分。这个应用程序可以是Web应用程序,但这次是一个手机应用程序(在文章中是Windows应用程序)。

在API使用场景中,流程可能会大致如下。

2017-11-22_105538.png

① 用户操作应用程序
② 应用程序想要使用API,因此访问API网关位于API服务器前面
③ 由于权限不足,返回401(未授权)
④ 应用程序发送认证和授权请求给OP,以获取对API的访问权限
⑤ OP对用户进行认证和同意处理

2017-11-22_105641.png

⑥ OP将授权码传递给应用程序
⑦ 应用程序从授权码获取访问令牌
⑧ 应用程序再次携带访问令牌访问API网关
⑨ API网关进行代理转发,因为这次是合法的
⑩ API只是简单地对请求返回响应。应用程序接收API的结果并将内容返回给用户。

建立环境

我们经过了一番长篇的铺垫,现在让我们来搭建环境吧。根据我们之前绘制的流程,除了作为OP的Keycloak之外,我们还需要API、API Gateway和API的客户端。让我们一个一个地创建它们吧(这个过程会比较冗长)。

使用巩(安装)

作为 API Gateway,我打算使用开源的 Kong(对于希望使用 AWS 的 API Gateway 的人,抱歉)。

Kong的结构

在安装Kong之前,让我们简单地介绍一下它的配置。如果不这样做,我们将不知道应该安装什么。Kong本身是一个常见的Web容器,最近它已经集成了Nginx。Kong的配置存储在外部数据库中(需要单独安装),根据文档所说,支持的数据库是 PostgreSQL 9.4或更高版本,或者Cassandra 3系(截至2017年12月)。在本文中,我们不需要特定的文档数据库,因此选择了PostgreSQL。

2017-11-22_105828.png

Kong的配置是属于最近常见的方式,其与用户(API客户端)访问的模块位于同一地方,只是端口不同而已。这样做会有困扰的地方,因为Kong是被用户(API客户端)访问的,不得不放置于DMZ,可能会给网络管理员带来困扰。由于端口不同,所以需要努力配置防火墙,不过Kong的管理功能可能会暴露在外部,有点不舒服。不过在未来的Kong版本中可能会进行分离。

默认端口是用户(API客户端)使用的号码为8000(http)和8443(https),管理员使用的号码是8001(http)和8444(https)。这些端口号可以通过Kong的设置进行更改,但在本文中为了方便起见,我们将继续使用默认设置。换句话说,用户(API客户端)的请求将通过端口8000访问API。

Kong的安装

尽管前面的话有点长(第二次),但终于可以开始安装了。如果想查看官方的安装步骤,请点击这里。对那些想快速搭建三行就行的人,可以使用Docker提供的方式。

    PostgreSQLのインストール

PostgreSQL的安装本身与普通安装没有任何区别。由于我是MySQL用户,所以对于详细的设置(特别是权限方面)并不太了解,所以可能会比较随意。如果您熟悉PostgreSQL,那么请合理地进行设置即可。

从这个链接下载RPM来获取二进制文件。在这篇文章中,我选择了9.6版本,没有特别深入的原因。由于不太清楚应该安装哪个RPM文件,所以我只安装了除了-devel以外的部分,看起来只需要安装主体和服务器就足够了。

$ yum install pgdg-centos96-9.6-3.noarch.rpm
$ yum install postgresql96 postgresql96-server postgresql96-libs postgresql96-contrib
$ /usr/pgsql-9.6/bin/postgresql96-setup initdb

我也会设置数据库的权限。由于PostgreSQL只能从Kong本体访问,所以只需要在localhost(127.0.0.1)上进行即可;但是为了方便,在本文中我将无限制地让其它人可以查看数据库内容。此外,我希望设置一个带密码的认证方式(md5),但是由于无法停止与操作系统用户的绑定,我放弃了这个想法(trust)。

# IPv4 local connections:
-host    all             all             127.0.0.1/32            ident
# IPv4 local connections:
+host    all             all             0.0.0.0/0               trust

此外,在PostgreSQL中,默认只监听localhost,因此也需要将其更改为其他值。

# - Connection Settings -
-#listen_addresses = 'localhost'
# - Connection Settings -
+listen_addresses = '*'

你可以用这个来启动服务。

$ systemctl enable postgresql-9.6
$ systemctl start postgresql-9.6

我将创建一个与Kong连接的PostgreSQL用户:kong。

$ psql -U postgres
postgres=# CREATE USER kong; CREATE DATABASE kong OWNER kong;
postgres=# \l
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 kong      | kong     | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
    Kongのインストール

从 Kong 安装页面下载 RPM。在本文中,我们选择了 CentOS7 版本。截至2017年12月,Kong 的版本为 0.11.1。下载后只需执行 yum 安装。

$ yum install kong-community-edition-0.11.1.el7.noarch.rpm

由於已將數據庫配置為使用Kong的默認配置,因此您現在可以啟動Kong,但如果更改了PostgreSQL的用戶名或設置了密碼,則需要修改Kong的配置文件。

Kong 的配置文件实际上不是默认的,因此它将使用所有默认值(而不是错误)。因此,如果要更改配置,请复制模板文件(/etc/kong/kong.conf.default)。配置文件应该放在 /etc/kong/kong.conf 中(在 Kong 的启动选项中,可以指定路径)。

$ cp /etc/kong/kong.conf.default /etc/kong/kong.conf

请查看文档以了解Kong配置文件的说明。关于连接到PostgreSQL的配置,请设置以下值即可。

database = postgres
pg_host = ...
pg_port = ...
pg_user = ...
pg_password = ...
pg_database = ...

迁移完成后,启动服务。

$ kong migrations up
$ kong start

如果要指定Kong的配置文件,则需要在选项中加入 -c (配置文件的路径)。顺便提一下,使用kong stop命令可以停止Kong,使用kong restart命令可以重新启动。

    Kongの起動を確認をする

我将使用curl来测试。端口用于管理,所以是8001。会出现一些较长的JSON格式的内容(是Kong的配置信息)。

$ curl -X GET http://127.0.0.1:8001
{"version":"0.11.1","plugins": ...(超長い)...

Kong安装完成了。

应用程序编程接口服务器。

我们也准备了一个在Kong上进行防御的API。我们期望这个API能够在API Gateway上进行防御,并且在没有任何访问限制的虚假应用程序中使用。这个API的内容只是一个在Apache服务器上运行的应用程序,仅仅返回一个简单的JSON对象,甚至只是简单的静态内容(plain/text)。

2017-11-15_123957.png

我设置了以下终端点。典型的的API通常进行CRUD操作,因此我准备了四个终端点。其中的“允许的方法”表示作为API,只允许使用该方法,即使权限受限,什么也不做(默认情况下,Apache不允许使用PUT和DELETE方法)。

エンドポイントURL許可する予定のメソッド説明http://172.26.22.66/r/GET何かをREADするエンドポイントhttp://172.26.22.66/c/POST何かをCREATEするエンドポイントhttp://172.26.22.66/u/PUT何かをUPDATEするエンドポイントhttp://172.26.22.66/d/DELETE何かをDELETEするエンドポイント

我们试试看,即使是这样的API,Kong是否能够正确地进行访问限制。

API客户端

根据之前描述的一般结构,API客户端可以是Web应用程序,但在这篇文章中选择了Windows应用程序。在OAuth2.0的背景下,智能手机应用程序也是本地应用程序,因此在Windows应用程序中的方法和思路与智能手机应用程序完全相同。

2017-11-15_163614.png

这个应用程序的左侧表单,将API的端点URL指定在顶部,当按下“发送”按钮时,它会访问该端点。下面是一个文本框,直接输出API的响应。请将其想象成一个手机应用程序的屏幕。右侧的无模态对话框是用于输入OP(=Keycloak)配置的屏幕,实际上不是要求用户输入的项目。因此,它被移至右侧显示。这个应用程序是为了测试而可输入的(尽管输入项目不多),实际上的设置是在安装时或者应用程序中硬编码的,然后应用到应用程序中。

简要解释了这个自制应用程序的操作方式。当访问API时返回401(未授权)时,它将开始向OP请求授权码流程。一旦从OP获取到授权码并获得访问令牌,它将再次附加访问令牌并访问API。如果不是401(未授权),它只会将API的响应显示在显示区域(表单下方的文本框)中。由于无法在这篇文章中完整解释授权码流程,请参阅两天前的某篇文章。

作为此应用程序的功能验证,我们将直接访问API(http://172.26.22.66/r/)。目前暂时省略右侧窗口的相关内容。从下面我们可以看到,第一行显示状态码,第二行及之后显示响应主体。

2017-11-15_175046.png

我也会尝试通过POST请求。由于API没有进行任何防御措施,所以返回的内容与GET请求完全相同。

2017-11-15_175403.png

Keycloak(OP)的配置

由于API客户端也是OAuth2.0客户端,因此需要在Keycloak中进行配置。已创建客户端ID为”kong”。其他参数设置如下。

2017-11-16_104039.png

有一点需要注意,重定向URI已设置为http://localhost。当然,由于没有将Web服务器安装在执行API客户端的Windows PC上,因此不存在重定向目标。稍后将对此进行说明。

整理环境

环境已经设置完成,但由于涉及到四个角色的复杂性,我会整理一下服务器信息。

サーバー名役割接続先URLAPIサーバーAPI提供http://172.26.22.66KongAPI Gatewayhttp://127.26.22.29:8000KeycloakOPhttp://172.26.22.5/auth/realms/masterWindowsアプリAPIクライアント自分のPC

我在Kong上尝试进行防御

只有简单的代理(尚无防御)

终于到达了Kong的配置。首先,只尝试设置API代理。由于Kong没有管理界面,只能通过curl直接调用REST API。管理界面将来可能会被实现(企业版中有)。

我要在Kong上注册API。我想要的设置是使用代理将http://172.26.22.29:8000/r/重定向到http://172.26.22.66/r/,并且只允许使用GET方法。(※我已经对响应体的JSON进行了格式化,以便于阅读)

$ curl -i -X POST \
  --data "name=myapi-read" \
  --data "hosts=172.26.22.29"
  --data "upstream_url=http://172.26.22.66/r" \
  --data "uris=/r" \
  --data "methods=GET" \
  http://172.26.22.29:8001/apis

HTTP/1.1 201 Created
Date: Wed, 15 Nov 2017 09:07:24 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.11.1

{
    "created_at": 1510736844046,
    "strip_uri": true,
    "id": "3d4772a4-9826-4691-8809-ccbc9f62a831",
    "hosts": [
        "172.26.22.29"
    ],
    "name": "myapi-read",
    "methods": [
        "GET"
    ],
    "http_if_terminated": false,
    "preserve_host": false,
    "upstream_url": "http://172.26.22.66/r",
    "uris": [
        "/r"
    ],
    "upstream_send_timeout": 60000,
    "upstream_connect_timeout": 60000,
    "upstream_read_timeout": 60000,
    "retries": 5,
    "https_only": false
}

我觉得Kong的管理员认证尚未实现,但迟早会有的。参数有点儿特殊。

リクエストパラメータ名説明nameAPIの名前(何でも良い)upstream_urlプロキシー先のURLhostsKongが受けつける、リクエストのHostヘッダの値。複数ある場合はカンマ区切りurisKongが受け取るパスmethods許可するメソッド。複数ある場合はカンマ区切り

由於uris為“/ r”,因此Kong將在http://172.26.22.29:8000/r上接收並代理到upstream_uri的http://172.26.22.66/r。我們可能會覺得奇怪的是hosts的存在。因為我們是發送請求給Kong,所以Kong的FQDN應該等於Host標頭的值,這個值的存在意義可能不太清楚。這可能是指類似於Apache中的虛擬主機(ServerName)的設置。

现在我们来把另外三个端点(/c,/u,/d)也注册到Kong中吧。其实我本来是希望将Kong上的设置都放在一个API中的,但是如果URL和方法都不同的话,我想不到如何将它们合并成一个设置。不过很快就会有API组的实现了。将三个curl命令排列在一起只会让代码看起来混乱,所以我会将设置放在表格中。

namehostsurismethodsupstreammyapi-read172.26.22.29/rGEThttp://172.26.22.66/rmyapi-create172.26.22.29/cPOSThttp://172.26.22.66/cmyapi-update172.26.22.29/uPUThttp://172.26.22.66/umyapi-delete172.26.22.29/dDELETEhttp://172.26.22.66/d

那我们来试一试吧。 我们不需要右窗户,所以省略掉它。

    GET
2017-11-16_105645.png
    POST
2017-11-16_105730.png

POST请求已经被很好地防御了。值得一提的是,状态码为404(未找到)并且正文内容为”no API…”,这是Kong的设计规范。

为了那些不相信你开发的应用程序的人,我将附上curl的结果。

$ curl -i -X GET http://172.26.22.29:8000/r/
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 15
Connection: keep-alive
Date: Thu, 16 Nov 2017 02:03:34 GMT
Server: Apache/2.4.25 (Unix) OpenSSL/1.0.2k-fips
Last-Modified: Mon, 13 Nov 2017 08:55:41 GMT
ETag: "f-55dd96f59dd40"
Accept-Ranges: bytes
X-Kong-Upstream-Latency: 2
X-Kong-Proxy-Latency: 0
Via: kong/0.11.1

{"hoge","READ"}


$ curl -i -X POST http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:39 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}


$ curl -i -X PUT http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}


$ curl -i -X DELETE http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:50 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}

保护API

接下来,让我们对API进行防御设置。

    plugin:jwtを設定する

为了保护API,我们会在其上安装JWT插件。但是JWT不是访问令牌啊!好吧,Keycloak的访问令牌是JWT,所以应该没有问题。通常的OP不使用JWT作为访问令牌,可能会选择使用ID令牌。(*对响应的JSON进行格式化)

$ curl -X POST http://172.26.22.29:8001/apis/myapi-read/plugins --data "name=jwt"
{
    "created_at": 1510798731000,
    "config": {
        "secret_is_base64": false,
        "key_claim_name": "iss",
        "anonymous": "",
        "run_on_preflight": true,
        "uri_param_names": [
            "jwt"
        ]
    },
    "id": "bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c",
    "name": "jwt",
    "api_id": "8478b314-f0aa-49b7-804f-ce0320d4461c",
    "enabled": true
}

对于API名称为myapi-read的API,启用plugin:jwt。name=jwt是固定值。虽然已经使用了该方式进行了保护,但没有设置授权条件,所以始终会返回401(未授权)。

    consumerを設定する

由于文档中没有任何解释,”consumer”这个词的意义不明确,但它表示使用API的人(消费者)。我将在Kong的API参考指南中用三行来解释一下。

Consumer对象表示API的消费者或用户。您可以选择将Kong作为主要数据存储,或者将消费者列表与您的数据库进行映射,以实现Kong和您现有的主要数据存储之间的一致性。

$ curl -X POST http://172.26.22.29:8001/consumers --data "username=myapi"
{"created_at":1510800004000,"username":"myapi","id":"087f5be4-cff5-4e40-b8a0-9b2925712834"}

对于参数“username”,可以使用任何消费者的名称作为值。

    JWTの署名を検証する公開鍵を設定する

首先,让我们从Keycloak获取公钥。由于这是用于对Keycloak发放的令牌进行签名,因此应该使用位于客户端之外的领域设置的公钥。从菜单中选择”领域设置” > “密钥”选项卡,然后点击”RSA”类型的”公钥”按钮,将公钥的值复制下来。

2017-11-16_115916.png
2017-11-16_120001.png

获得公钥后,可以使用curl进行注册,操作和往常一样。由于公钥格式要求使用PEM形式,所以需要稍微加工一下。将刚刚复制的公钥粘贴到文本中,并添加标题和尾部。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq/3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v+3gzKzzVPx+7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR/YuPHxbK9FF2ssNkfOwWxpfYBl/oKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh+OxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB
-----END PUBLIC KEY-----

提供给那些对 PEM 有痴迷之情的人的补充说明是,PEM 是 MIME 格式,所以每行需要换行符,限制为 76 字节。但请注意,实际上这是不必要的。因为我们将头部和尾部连接在一起后,会通过 curl 发送,并进行 URL 编码。

-----BEGIN+PUBLIC+KEY-----%0D%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq%2F3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v%2B3gzKzzVPx%2B7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR%2FYuPHxbK9FF2ssNkfOwWxpfYBl%2FoKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh%2BOxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB%0D%0A-----END+PUBLIC+KEY-----

现在你终于可以使用curl发送了。

curl -X POST http://172.26.22.29:8001/consumers/myapi/jwt \
  --data "key=http://172.26.22.5/auth/realms/master" \
  --data "algorithm=RS256" \
  --data "rsa_public_key=-----BEGIN+PUBLIC+KEY-----%0D%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq%2F3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v%2B3gzKzzVPx%2B7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR%2FYuPHxbK9FF2ssNkfOwWxpfYBl%2FoKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh%2BOxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB%0D%0A-----END+PUBLIC+KEY-----"

{
  "rsa_public_key": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq/3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v+3gzKzzVPx+7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR/YuPHxbK9FF2ssNkfOwWxpfYBl/oKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh+OxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB\r\n-----END PUBLIC KEY-----",
  "created_at": 1510801636000,
  "id": "45cc56d0-660f-41dd-a5ab-6a833ecdfbd4",
  "algorithm": "RS256",
  "key": "http://172.26.22.5/auth/realms/master",
  "secret": "cOjsJz2daBE73YapxhHmde7YMchU4uK1",
  "consumer_id": "087f5be4-cff5-4e40-b8a0-9b2925712834"
}

参数的意思如下。

パラメータ名説明key検証するJWTの属性値algorithm署名のアルゴリズム名rsa_public_key署名を検証するRSA公開鍵

在这些参数中,最重要的参数是key,你需要将设定的值和JWT的值进行比较,来判断是否允许或拒绝。JWT的属性是什么?它是在plugin:jwt中设定的”key_claim_name”,默认值是iss。换句话说,将会通过字符串的完全匹配,来比较JWT的iss值和在这里设定的key值。Keycloak颁发的访问令牌JWT的iss值是”http://172.26.22.5/auth/realms/master”,因此将其设定为这个值。

:information_source: JWTの検証は、

  • “key_claim_name”で指定した属性値の完全に一致
  • JWTの署名の検証

が行われます。JWTの有効期限(“exp”)は検証されません。

    試してみる

設定已經完成,現在試試看吧。為了更加清楚地了解應用程式的操作,我們再次附上流程圖。

2017-11-22_105538.png

这次在右窗口中输入OP的设置,并从应用程序访问Kong(http://172.26.22.29:8000/r/) (①②)。由于Kong没有访问令牌,因此会返回401(未经授权)(③)。应用程序会向Keycloak发送授权请求以获取访问令牌(④)。由于Keycloak未经认证,因此会返回登录页面(⑤)。

2017-11-16_161733.png

在这个应用程序中,我们将Keycloak的界面显示在应用程序内部(通过将内部转换为TextBox控件的WebBrowser控件)。登录界面没有很好地适应响应式设计,但我们选择不予理会。

2017-11-22_105641.png

当在登录页面输入用户名和密码进行登录后,由于目前在Keycloak上的设定不会显示同意画面,授权码将会被发送到重定向URI上(⑥)。接收授权码的重定向URI是…http://localhost。也就是说,WebBrowser控件试图访问http://localhost,却会显示”无法显示该页面”的提示,但应用程序实际需要的是授权码,它可以通过查询参数从WebBrowser控件的Uri属性中获取。也就是说,如果让应用程序显示内嵌浏览器,将可能绕过重定向URI(这也是内嵌浏览器存在安全问题的原因之一)。

在安全方面暂且不谈,可以通过绕过重定向URI来窃取授权代码,从而获取访问令牌(⑦)。一旦获得访问令牌,再次访问Kong(⑧)。这次Kong将进行授权,然后将API作为代理(⑨),这样就能够成功从API获得响应(⑩)。

2017-11-16_163607.png
:warning: この記事のアプリケーションはテスト用であるため、雑な作りになっていますが、本来はアプリケーション上にHTMLを表示してしまうOAuth2.0クライアントアプリケーションはセキュリティ上問題があります。理由は、

  • client_secret をアプリケーションに埋め込んでしまう(アプリケーションを解析することでclient_secretがばれてしまう。
  • リダイレクトURIを踏み倒している。(アプリの開発者はリダイレクトURIが何の検証にも役に立っていないことを知っている)
  • ユーザーにどのURLにアクセスしているかが分からない。(URLバーをアプリに付けても、アプリが勝手に表示しているだけなので、いくらでも詐称できる)

といったものがあります。セキュアにやるには、外部ブラウザ(http(s)://~で起動するアプリ)を起動して認証・認可リクエストを投げます。リダイレクトURIには、アプリケーションが受け取れるようにカスタムスキーマをOSに登録します。カスタムスキーマとは、例えばブラウザのURLに hoge://~ と入力すると、アプリが起動するように関連づけておくことです。 リダイレクトURIには、スマホアプリの場合iOS Universal LinksAndroid App Linksなど、特定のURLをアプリに紐付ける仕組みを利用します。Windowsアプリケーションの場合は、WebBrowserコントロールを使えばURLを直接取得できるので、そのまま利用します。また、client_secretが秘密に保てないので、このOAuth2.0クライアントは publicクライアントとして扱う必要があります。スマホアプリでセキュアにOAuth2.0 / OpenID Connectで連携する方法は、これだけで大きなテーマであるため、この記事では割愛させていだたきます。

    認可周りの設定の関連(Kongのデータモデル)

关于GET请求的端点(http://172.26.22.29:8000/r/),现在这样就可以了,但是POST请求(http://172.26.22.29:8000/c/)、PUT请求(http://172.26.22.29:8000/u/)和DELETE请求(http://172.26.22.29:8000/d/)的端点还没有设置。针对API:myapi-create、myapi-update和myapi-delete,需要分别添加“plugin:jwt”的配置。关于“consumer”和“公开密钥的配置”,可以使用与GET相同的配置,所以不需要为每个API添加额外的配置。

由于涉及到许多角色,因此在这里只简单地描述相关的设置。在本篇文章的示例中,我们对apis进行了四项API设置以对应到四个端点。plugins通过apis:plugins = 1:1的关系进行设置,总共四个设置。consumers与plugins没有联系,只进行了一次设置。jwt_secrets通过consumers:jwt_secrets = 1:1的关系进行了一次设置。

2017-11-22_111124.png

插件可以与消费者建立一对一的关系,也可以无关。在本文的例子中,消费者理论上不应该是必要的,但是由于jwt_secret的设置需要消费者,因此我们设置了消费者。在将来的Kong版本中可能会进行更改。

以用户为单位进行防御

Keycloak可以通过发行的访问令牌(即JWT)来进行API防御。然而,这个判断方法是通过JWT的iss值实现的,这个值是Keycloak特定的值,因此始终相同(准确地说,在每个领域中的值是不同的)。换句话说,只要有Keycloak分发的访问令牌,任何人都可以访问API。虽然在此级别的防御已经足够,但既然我们已经进行了授权,那就再努力一点吧。让用户决定是否允许访问API。

    JWTの指定した属性を見るようにする(plugin)

用户无法以单个用户为单位进行访问限制控制,是因为它是根据JWT的iss值进行判断的,如果改为根据用户名或用户ID进行判断就可以了。插件中有一个设置来查看iss值,所以我们将对此进行更改。此外,Keycloak访问令牌中包含用户名的属性很重要,preferred_username看起来是个不错的选择。

照常使用curl进行设置。

$ curl -i -X PATCH --data "config.key_claim_name=preferred_username" \
"http://172.26.22.29:8001/apis/myapi-read/plugins/bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c"
HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 02:35:25 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.11.1

{
    "created_at": 1510798731000,
    "config": {
        "key_claim_name": "preferred_username",
        "secret_is_base64": false,
        "anonymous": "",
        "run_on_preflight": true,
        "uri_param_names": [
            "jwt"
        ]
    },
    "id": "bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c",
    "name": "jwt",
    "api_id": "8478b314-f0aa-49b7-804f-ce0320d4461c",
    "enabled": true
}

参数有点复杂,但是URL里的myapi-read是API的名称,…/plugins/后面的bb3c…是插件的ID。由于某种原因无法指定插件名称(jwt),所以我通过ID进行指定。配置值本身是通过请求体中的config.(属性名)进行指定的。这次我想把key_claim_name设置为iss→preferred_username,所以指定config.key_claim_name=preferred_username。顺便一提,HTTP方法是PATCH而不是PUT。Kong的更新API中混合了PATCH和PUT,有点杂乱无章。

如果一个人想要彻底删除并重新创建,他可以通过使用“DELETE”来实现。

$ curl -i -X DELETE http://172.26.22.29:8001/apis/myapi-read/plugins/bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c
    比較する値を変更する(jwt_secret)

由于更改了要比较的属性名称,因此需要更改属性值本身。这是在设置了公钥的地方,通过key的值来进行设置。我们将只允许用户u001访问(请在Keycloak上创建该用户)。

$ curl -i -X PUT --data "key=u001" http://172.26.22.29:8001/consumers/myapi/jwt

{
    "created_at": 1510887959000,
    "id": "7c493182-f49d-4cd8-8cf1-a197f4763cce",
    "algorithm": "RS256",
    "key": "u001",
    "secret": "QLgb9vQK0r9YbSD0b0MP0U6IPBFEzCpl",
    "consumer_id": "087f5be4-cff5-4e40-b8a0-9b2925712834"
}
    試してみる

让我们试试看。首先,我们将尝试使用用户u001进行Kong认证。

2017-11-17_121349.png
2017-11-17_121413.png

我已经顺利地获得了批准。接下来我将以没有权限的用户u111进行尝试。

2017-11-17_121654.png
2017-11-17_121711.png

没有通过认证,出现了403(禁止访问)。

总结

由于这篇文章变得很长,所以我想把它分成几个要点总结一下。

    Kongの認可について(JWT検証)

这次进行的认证只是简单地匹配了JWT中的属性值,而不是OAuth2.0。属性值的验证条件将来会实现更灵活的设置,不仅仅是匹配,可能会包括正则表达式等。然而,我们最想要的验证是scope的值,scope=foo bar和scope=bar foo表示相同的权限,但目前无法将这两者视为相同的设置,而且正则表达式也无法实现(或者说非常困难),对此我们关注的是将来的版本是否会实现到这个程度。

另外,使用JWT进行验证也有些不便,因为访问令牌通常只是一个普通的字符串,而不是JWT。由于Keycloak规定访问令牌必须是JWT格式,所以在Kong上进行授权时无需考虑太多,但大多数OP或OP产品可能不会那么简单。虽然ID令牌是JWT格式,但考虑到ID令牌的目的(用于认证)和令牌的有效期限(没有刷新机制),我觉得有点不太方便。

    Kongの認可について(アクセストークン)

在OAuth2.0认证中,授权仍然是使用访问令牌的方式,所以我希望Kong能够将从RP获取的访问令牌发送到OP的令牌检查端点,以获取权限信息。实际上,在数据库中我发现了一个名为oauth2_xxx的表,因此可能已经实现了此功能,但由于文档中尚未提到,我不清楚该如何进行设置和使用。我相信在不久的将来会有文档对此进行说明,届时我再试一试应该会好一些。

2017-11-22_112756.png
    Kong本体について

截至2017年12月,版本号为0.11.1,但仍然感觉为时过早。文档写得还可以,但只有API参考文档是完整的,缺少示例使事情相当艰难。此外,缺乏管理界面(GUI)在考虑实际运营时相当困难,这确实是我们迫切需要的部分。就像本次使用curl输入的最简单的例子一样,可能相当困难。顺便提一下,我直接更新了数据库。

    全体として

这篇文章只讨论了最简单的认可示例,并且仅关注认可功能,却变成了一篇很长的文章。虽然其中大约一半是废话。其实我也想看看日志和负载均衡等方面的内容,但希望在另一个机会再讨论。

可能的中国翻译:

也许Kong的直接竞争对手目前是AWS API Gateway,尽管功能方面可能有所不同(一年后,Kong很可能会实现相当多的功能,并且可能与API Gateway的功能差别不大),易于配置和使用性将是比较的重点。我认为能否进行非编程设置,包括脚本在内,可能是关键。对于系统开发人员来说,编写一两个脚本可能是轻而易举的,但实际上最常使用的人是运营人员,不能忽视这一点。

另外,我认为有些情况下可能没有选择的余地,仅仅是因为不能使用AWS这个要求。对于这些人,我希望Kong 1.0版本能够尽快发布。

请提供参考资料。

    QiitaにあるKongの記事

在Qiita上已经有人试过Kong。由于Kong的文档没有提供示例,只给出了API参考指南,所以这对我非常有帮助。

    Kongのドキュメント

我不能不看。

    KeycloakでKongと連携する

你知道需要使用什么样的API。

    NRI OpenStandia Keycloak日本語ドキュメント
bannerAds