有些时候你会为一个产品制作很多功能,而这些功能往往需要相同的用户认证系统,相同的接口统计系统。为每个功能都复制模块代码实在不是什么好主意。
这个时候,你需要一个API网关,统一管理各个接口的各种功能,比如SSL证书,OAuth2验证等。
本文编写时 Kong 版本为 0.9.5,本文适用于 0.9.5-0.9.9 之间的版本。目前 Kong 已升级到 0.10,API变化很大,本文的部分内容将不再适用,请读者注意!
概念
- Kong
Kong 是一个开源的 API网关,你也可以认为他是一个 API中间件。对于一个API中间件,Kong将此思维发挥到了极致,所有对Kong配置的增加和修改,也都是依靠 RESTful API 来操作。
Kong 非常适合于管理后端基于 Docker 等以微服务形式实现的 API 接口。官网:https://getkong.org/
本文将基于 Kong 0.9.5 版本进行实现。
- OAuth2
OAuth2 我就不讲是什么了
实现Kong完成API转发
需求和现状
我们将要为以下服务实现API请求转发:
- Web服务X,地址为10.0.0.100:8081
- Web服务Y,地址为10.0.0.100:8082
- Web服务Z,地址为10.0.0.100:8083,此服务器提供用户信息服务,可用于保存或验证用户的账号密码。
Kong服务器地址:10.0.1.0,域名:www.your-kong-domain.com
安装和启动
本文的安装全部使用Docker进行安装。对于Docker的使用方式,请参见我之前的文章。
- 安装并启动cassandra数据库
1 |
docker run -d --name kong-database -p 9042:9042 cassandra:2.2 |
注意这个服务启动的速度非常慢,我在阿里云上启动这个服务,9042端口生效需要18秒(cassandra官网给出的wait-for-it.sh只等了15秒就受不了了),启动真的很慢,太着急而启动 Kong服务 的话会出现数据库连接失败而推出的问题。
- 安装并启动Kong
1 |
docker run -d --name kong --link kong-database:kong-database -e "KONG_DATABASE=cassandra" -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" -e "KONG_PG_HOST=kong-database" -p 80:8000 -p 443:8443 -p 8001:8001 -p 7946:7946 -p 7946:7946/udp kong |
这个服务启动也很慢,急着进行下一步启动 kong-dashboard 连接端口也会报错,不能着急。kong 的 docker 版本做的还不够好,日志并没有连到标准输入输出上,需要的话可以进入容器内查看。日志地址:/usr/local/kong/logs/
- 安装并启动 kong-dashboard
由于 Kong 对 RESTful API 的极致使用简直到了强迫症的程度,所以对于某些操作,有一个高效的 UI 界面要比在纯命令行模式下更容易观察和操作。
kong-dashboard是一个开源的Kong UI管理工具。其UI使用的框架是我非常喜欢的 Materialize。项目地址:https://github.com/PGBI/kong-dashboard
1 |
docker run -d -p 8080:8080 pgbi/kong-dashboard |
打开Kong服务器的8080端口,就可以进入 kong-dashboard 的界面了。
填写我们的Kong管理地址和端口8001,就可以进入管理页面了。
策略模式
官方文档:https://getkong.org/docs/0.5.x/admin-api/
为了便于理解,将Kong目前能实现模式简单分为两种,以便于理解:Request host 和 Request path。
Request host:
1 2 3 4 5 6 7 8 |
- www.domain1.com/api1 → s1.domain1.com/api1 - www.domain1.com/api2 → s1.domain1.com/api2 - ... - www.domain1.com/* → s1.domain1.com/* - www.domain2.com/api1 → s2.domain1.com/api1 - www.domain2.com/api2 → s2.domain1.com/api2 - ... - www.domain2.com/* → s2.domain1.com/* |
要在Kong服务器上绑定多个域名。(提供api服务的服务器可以没有域名绑定,只要有IP即可)
Request path
1 2 3 4 5 6 7 8 9 |
- www.domain1.com/s1/api1 → s1.domain1.com/s1/api1 - www.domain1.com/s1/api2 → s1.domain1.com/s1/api2 - ... - www.domain1.com/s1/* → s1.domain1.com/s1/* - www.domain1.com/s2/api1 → s2.domain1.com/s2/api1 - www.domain1.com/s2/api2 → s2.domain1.com/s2/api2 - ... - www.domain1.com/s2/* → s2.domain1.com/s2/* |
Kong服务器上只需要绑定一个域名,不同的API集合依靠不同的请求路径实现。(同上,提供api服务的服务器可以没有域名绑定,只要有IP即可)
Request host 和 Request path 可组合使用,但比较复杂,不建议使用。
新建API策略
我们新建一条名为service1的策略,将请求转发到 http://110.0.0.100:8081
两个参数:
- strip_request_path:在使用 Request path 模式会用到的参数。
如果开启时效果是这样的:
1 |
www.domain1.com/s1/api1 → s1.domain1.com/api1 |
不开启时则是:
1 |
www.domain1.com/s1/api1 → s1.domain1.com/s1/api1 |
根据你后端进行设计和配置。
我们这次使用的是Request host,用不到这个参数。(默认不开启)
- preserve_host:不开启此项的话,API服务器收到的请求中,HOST参数和SERVER_NAME为API服务器。开启则为Kong服务器绑定的域名。(默认不开启)
点击保存后,访问 kong.yourname.com,就能打开 http://110.0.0.100:8081 了。
开启HTTPS
使用 OAuth2 的前提便是你的接口是HTTPS的,所以我们先开启HTTPS。
貌似 kong-dashboard 的 ssl 插件也可以实现,不过为了理解下Kong的RESTFUL API,所以添加SSL证书的步骤,我们全部都采用使用命令行添加的方式。
Let’s encrypt 人工申请证书
(如果暂时没有域名,或者不打算麻烦 Let’s encrypt 或其他认证机构,也可以跳过,kong的https端口上默认绑着localhost的自签证书用于开发环境和测试。可以跳过此段)
虽然 Let’s encrypt 提供了非常多的方法让大家申请可信任的 ssl证书,然而我还是更喜欢人工申请证书文件,手动导入到服务器。这么做比较有真实感,另外因为服务器总要换来换去,总麻烦人家 Let’s encrypt 也不好,还是证书文件最实在。
这里就不解释申请的方法了。
最终你会得到四个文件:
1 |
cert1.pem chain1.pem fullchain1.pem privkey1.pem |
证书文件、链文件、全链文件、私钥文件。证书和私钥就不解释了,全链文件就是证书和私钥加一起,适用于比较新的http服务器。链文件是啥至今没搞懂,也不知道在哪用。
使用SSL插件导入证书
注意:SSL插件只适用于 request_host 模式!!!
1 2 3 4 |
curl -X POST http://api.your-kong-domain.com:8001/apis/node1/plugins \ --form "name=ssl" \ --form "config.cert=@/root/ssl/cert1.pem" \ --form "config.key=@/root/ssl/privkey1.pem" |
访问你API的HTTPS地址:https://api.your-kong-domain.com/node1/,检查证书绑定情况。
对于 request_path 模式,不需要使用插件,只需要修改 Kong 的配置文件即可。这个部分没有提供 API,需要手动修改kong.conf。相关文档:https://getkong.org/docs/0.9.x/configuration/#ssl_cert_path
实现OAuth2 验证
我们来实现一个 WebApp 常用的 密码模式(Resource Owner Password Credentials Grant)OAuth2 接口验证。
这个实例与官方实例略有不同。官方的用户认证服务器在Kong的外侧,用户直接访问。而我们的例子中,用户认证服务器在Kong的内侧,我们需要通过Kong进行访问。其他条件没有区别,整体核心思路是相同的。
整体流程图
解释:
- 用户发送账号密码到 Kong服务器的用户验证API;
- Kong服务器将请求1转发给用户验证服务器;
- 用户认证服务器验证用户账号密码是否正确,如果不正确直接返回错误结果并完成用户认证;如果正确则将:用户账号密码、client_id和client_secret、scope、provision_key、用户ID 发送给 Kong 的OAuth2 接口
- Kong生成access_token并返回给用户认证服务器,并缓存与过期时间相同的 scope,用户ID
- 认证服务器将4中收到的access_token返回请求2给Kong,Kong再返回请求1给用户。
- 用户得到access_token之后,就可以访问有OAuth2验证的接口了。access_token中已隐含了用户ID和scope。
实现
增加consumer
添加一个consumer,设置Username:oauth_test,Custom id:100。点击保存。
回到comsumers列表,点击编辑刚才添加的oauth_test。在最下方点击OAUTH2,新建oauth2 credentials。
- Username:oauth_test_credential
- Client_id:留空,自动生成
- Client_secret:留空,自动生成
- Redirect URI:密码模式在这里用不到,但是此字段不能为空,于是你随便填一个吧
点击创建。Kong会自动生成 Client ID 和 Client Secret。这两个信息将用于我们的 WebApp。
为接口增加OAuth2验证
我们为 策略 node1:https://www.your-kong-domain.com/node1 → http://10.0.0.100:8081/node1
添加 OAuth2 验证。
- mandatory_scope: 强制需要scope,至少需要一个scope来验证用户权限
- implicit_grant: 简化模式
- hide_credentials: 隐藏凭据,开启后,后端API就看不到access_token
- password_grant: 密码模式
- accept_http_if_already_terminated: 接受HTTP请求
- client_credentials: 客户端模式
- authorization_code: 授权码模式
我们需要勾选 hide_credentials 和 password_grant
准备用户认证服务器
新建一个API策略auth:https://www.your-kong-domain.com/auth/ → http://10.0.0.100:8083/auth/
并且在认证服务器上新建脚本test.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<?php $rst = array('error' => 1,'errnor' => 0); $username = $_GET['username']; $password = $_GET['password']; if ($username == 'abc' && $password == '123') { $rst['error'] = 0; $data = array('client_id' => 'a81c50f813d44781b74c7d2fe473f7a9', 'grant_type' => 'password', 'client_secret' => '8117c0592e214021884a6051d26158bf', 'scope' => 'test1_scope', 'provision_key' => 'provision-key-for-node1-oauth2', 'authenticated_userid' => '233', 'username' => 'abc', 'password' => '123', ); $url = 'https://www.your-kong-domain.com/node1/oauth2/token'; $msg = httpsRequest($url, $data); $rst['msg'] = $msg; } else { $rst['msg'] = 'Auth failed.'; } $result = json_encode($rst); echo $result; function httpsRequest($url, $data = null) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); if (!empty($data)) { curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } else { } curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($curl); curl_close($curl); return $output; } |
当用户名为abc且密码为123的时候就算账号密码正确,否则认证失败。
注意 $url 那里已经写死了 node1,对于实际使用时,应该在用户请求中将对应的api当做参数与用户名和密码同时传进来。
测试请求
1 |
curl 'https://www.your-kong-domain.com/auth/test.php?username=abc&password=123' |
最终会从msg中得到:
1 2 3 4 5 6 |
{ "refresh_token": "3a6f146a14e74c10a5884fc691380eac", "token_type": "bearer", "access_token": "2363f7e385ef4079a1525f3d95e14c03", "expires_in": 7200 } |
现在向需要OAuth2认证的接口写一个新的php文件:
1 2 3 4 5 6 7 8 9 |
<?php header('Content-Type: text/plain; charset=UTF-8'); echo 'request header:'; print_r(getallheaders(), true); echo '$_GET:'; print_r($_GET, true); |
发起OAuth2请求:
1 |
curl -k 'https://www.your-kong-domain.com/detail/detail.php' -H 'Authorization: Bearer 2363f7e385ef4079a1525f3d95e14c03' |
响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
request header:Array ( [X-Real-IP] => 61.148.52.162 [X-Forwarded-For] => 61.148.52.162 [X-Forwarded-Proto] => https [Connection] => close [User-Agent] => curl/7.29.0 [Accept] => */* [X-Forwarded-Prefix] => /detail [X-Consumer-ID] => 78b2ae09-1ef4-4dcb-8060-d469cdef8984 [X-Consumer-Custom-ID] => 100 [X-Consumer-Username] => oauth1 [x-authenticated-scope] => test1_scope [x-authenticated-userid] => 233 ) $_GET:Array ( ) |
可以在Header中看到,scope和userid已经传过来了。
整个基于Kong的OAuth2认证就完成了。
总结
目前 Kong 的版本已经升级到了 0.9.6,但仍有很多功能缺失。Kong 是基于 NGINX 开发实现的,然而很多 NGINX 已经实现的功能,比如 Websocket 转发,仍没有完全支持。下一个版本或许会开始支持 Websocket。OAuth2这里也有很多问题,在这里就不一一描述了。坑都不是很大,多数情况下都可以满足需求。
14 comments
Skip to comment form ↓
慕若曦
2016 年 12 月 25 日 在 上午 2:26 (UTC 8) Link to this comment
圣诞节快乐
石樱灯笼
2016 年 12 月 29 日 在 上午 12:33 (UTC 8) Link to this comment
域名留错了,pw怎么变com了
慕若曦
2016 年 12 月 30 日 在 下午 2:38 (UTC 8) Link to this comment
被浏览器的填充给坑了……
石樱灯笼
2017 年 1 月 1 日 在 上午 12:12 (UTC 8) Link to this comment
都被坑过
小彦
2017 年 1 月 11 日 在 下午 4:18 (UTC 8) Link to this comment
开懂一点点,大概是一个Auth服务相关的?
还有子域名路由和alias功能?
夏天烤洋芋
2017 年 1 月 13 日 在 上午 11:28 (UTC 8) Link to this comment
不知道这是干嘛的!
石樱灯笼
2017 年 2 月 8 日 在 下午 3:04 (UTC 8) Link to this comment
微服务架构的网站会用的东西
xiaopanp
2017 年 9 月 1 日 在 下午 8:09 (UTC 8) Link to this comment
有用过Kong+consul?
石樱灯笼
2017 年 9 月 1 日 在 下午 8:55 (UTC 8) Link to this comment
没有
viable
2017 年 10 月 10 日 在 下午 5:56 (UTC 8) Link to this comment
您好
请问 我ip:8080 填写 IP:8001 就出现 错误 什么什么原因? 楼主能帮忙解答嘛?
SparkleBO
2017 年 12 月 11 日 在 下午 6:53 (UTC 8) Link to this comment
Scope是什么意思啊
石樱灯笼
2017 年 12 月 11 日 在 下午 8:53 (UTC 8) Link to this comment
token的权限范围
xiaoxiao
2017 年 12 月 28 日 在 上午 12:37 (UTC 8) Link to this comment
后端有多个实例提供服务应该如何配置
石樱灯笼
2017 年 12 月 28 日 在 上午 1:11 (UTC 8) Link to this comment
当时研究 Kong 的时候,公司买的负载均衡,且实现非常不优雅,没有一丝参考价值,所以最终并没有写进文内。
至于 Kong 自己的负载均衡以及 后端API 的负载均衡,当时没有时间去学习。而由于公司项目组也没有活到项目开发完成,所以也没有再深入研究。
Kong v0.10 之后的版本与 v0.9 版本差异也很大,现在版本已经到 0.11.2 了,而且还分了社区版和企业版。想要使用的话建议重新学习新版本。