Skip to main content

使用 Keycloak 与 API 网关保护你的 API

· 阅读需约 10 分钟
朱欣欣
琚致远

本篇文章将一步一步引导你如何使用 API 网关与 Keycloak 保护你的 API。

OpenID Connect 又名 OIDC,是基于 OAuth 2.0 的认证协议。它允许客户端从身份认证服务 IdP 获取用户基本信息,常见的 IdP 有:Keycloak、Ory、Okta、Auth0、Authing 等。

开源的 API 网关 Apache APISIX 支持使用 openid-connect 插件对接以上身份认证服务,APISIX 会将所有未认证的请求重定向至身份认证服务的登录页,当登录成功后 APISIX 会将获取到的用户信息发送至上游服务。

screenshot
Click to Preview

Keycloak 是一个开源的身份认证管理服务,可作为 IdP 来使用。它为应用程序增加了认证服务,并能通过最小的代价保证服务的安全。同时,它还提供了用户联邦、强认证、用户管理、细粒度授权等功能。在这篇文章中,我们将以 Keycloak 为例,让我们看看如何将其与 APISIX 对接,以保护你的服务。

流程描述

下图为 OpenID Connect 协议交互流程:

screenshot
Click to Preview

在重定向阶段(Redirect),IdP 将用户重定向到一个预先配置好的重定向 URL(redirect_url),例如 http://127.0.0.1:9080/callback。请注意:这是一个在 APISIX 中不存在的 API,它只用于捕获相关的请求,并在 OIDC 逻辑中完成 Token 交换的功能。请不要使用这个地址作为触发 OIDC 插件重定向的条件,否则,它将返回如下错误:the error request to the redirect_uri path, but there's no session state found

相关术语

  1. Bearer Only: Keycloak 支持账户密码或 AccessToken 进行身份认证,若启用 bearer_only 选项,则仅允许通过 AccessToken 进行认证,该方式适用于服务之间的访问认证;
  2. Realm: Keycloak 中的 Realm 相当于一个租户,不同 Realm 是相互隔离的,只能管理和验证它们所具有的用户;
  3. Scope:这是一种限制在访问令牌(AccessToken)中声明的角色的方法。例如,当一个客户端要求验证一个用户时,客户端收到的访问令牌将只包含范围明确指定的角色映射。客户端范围允许你限制每个单独的访问令牌的权限,而不是让客户端访问用户的所有权限;
  4. User:User 是可以登录到 Keycloak 的用户,可以思考下你所用过的 SSO 登录服务;
  5. Client:客户端是指想要使用 Keycloak 来保护的服务。

前置步骤

  1. 本指南将使用 Docker 进行 Keycloak 的安装与启动;
  2. 本指南所用服务器环境为 CentOS 7,IP 为 127.0.0.1。

安装 Apache APISIX

请参考 https://apisix.apache.org/docs/apisix/getting-started 安装、启动 APISIX,本示例中 APISIX 实例地址为 http://127.0.0.1:9080/

配置 Keycloak

请在服务器中执行如下命令安装 18.0.2 版本的 Keycloak:

docker run -d -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:18.0.2 start-dev

启动完成后 Keycloak 将监听 8080 端口,并使用 admin 作为管理员账户与密码。

创建 Realm

访问 http://127.0.0.1:8080/ 将显示如下界面,这表示 Keycloak 已成功运行。

screenshot
Click to Preview

访问 Administrator Console 并使用 admin 作为账户与密码进行登录。

screenshot
Click to Preview

安装完毕首次登陆时,系统内将默认创建名为 master 的 Realm。Realm 是 OIDC 体系中的一个概念,可理解为租户。

screenshot
Click to Preview

鼠标移动到左侧 Master 上方时点击 Add realm 并输入 myrealm 作为 Realm 的名称,创建。

当安装 Keycloak 首次登录时,系统中会默认创建一个名为 master 的 Realm,但它是专门用来管理 Keycloak 的,我们不应该把它用于我们的应用,所以我们需要创建一个新的 Realm。

screenshot
Click to Preview

创建成功后将看到已切换至 myrealm,且底部存有 Endpoints -> OpenID Endpoint Configuration,地址为 http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration,这是一个服务发现的规范,稍后将使用该地址作为 OIDC 所需要使用的各个节点地址。

screenshot
Click to Preview

创建 User

我们需要创建一个用户用于通过账户密码进行登陆认证。选择 Manage -> Users -> Add user,并输入 myuser 作为用户名,保存后访问 Users 页面并选择 View all users

screenshot
Click to Preview

screenshot
Click to Preview

screenshot
Click to Preview

点击 ID 后进入 Credentials 选项卡,设置一个新密码(本示例使用 mypassword 作为密码)。同时将 Temporary 设置为 OFF,以关闭首次登陆必须修改密码的限制。

screenshot
Click to Preview

点击 Set Password 保存设置。

创建 Client

访问 Configure -> Clients -> Create 以创建一个新的 Client,这将用于之后配置 APISIX。

screenshot
Click to Preview

输入 Client ID 并保存,本例使用 myclient 作为 ID。

screenshot
Click to Preview

保存后需要配置 2 个参数:

  1. Access Type:默认为 public,请修改为 credential 以获取 Client Secret;
  2. Valid Redirect URIs:当登陆成功时,Keycloak 将携带 state 与 code 重定向客户端至该地址,因此设置为 Apache APISIX 的特定回调地址,例如 http://127.0.0.1:9080/anything/callback

screenshot
Click to Preview

设置完成后保存,此时页面顶部将出现 Credentials 选项卡,将 Secret 复制下来。

screenshot
Click to Preview

配置汇总

Apache APISIX

实例地址:http://127.0.0.1:9080/

Keycloak

  1. 服务地址:http://127.0.0.1:8080/
  2. Realm:myrealm
  3. Client ID:myclient
  4. Client Secret:e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq
  5. Redirect URI:http://127.0.0.1:9080/anything/callback
  6. Discovery:http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration
  7. Logout URL:/anything/logout
  8. Bearer Only: false
  9. Username:myuser
  10. Password:mypassword

场景示例

前置条件

本示例使用 httpbin.org/anything 作为上游服务,它将返回请求中的所有内容。

screenshot
Click to Preview

场景一:使用账户密码保护上游服务

本示例将引导客户端到登陆页通过账户密码的方式进行身份认证:

  1. 使用如下命令创建一条 API:
curl -XPUT 127.0.0.1:9080/apisix/admin/routes/1 -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -d '{
"uri":"/anything/*",
"plugins": {
"openid-connect": {
"client_id": "myclient",
"client_secret": "e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq",
"discovery": "http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration",
"scope": "openid profile",
"bearer_only": false,
"realm": "myrealm",
"redirect_uri": "http://127.0.0.1:9080/anything/callback",
"logout_path": "/anything/logout"
}
},
"upstream":{
"type":"roundrobin",
"nodes":{
"httpbin.org:80":1
}
}
}'

screenshot
Click to Preview

  1. 创建 API 成功后访问 http://127.0.0.1:9080/anything/test 时,由于未进行登录,因此将被引导到 Keycloak 的登录页面:

screenshot
Click to Preview

  1. 输入账号(myuser)、密码(mypassword)完成登录后,成功跳转到 http://127.0.0.1:9080/anything/test 页面:

screenshot
Click to Preview

  1. 访问 http://127.0.0.1:9080/anything/logout 退出登录:

screenshot
Click to Preview

场景二:使用 AccessToken 验证身份

通过启用 bearer_only 参数对应用之间的调用进行身份认证,此时应用访问 APISIX 时需携带 Authorization Header,否则该请求将被拒绝。

  1. 使用如下命令创建一条 API:
curl -XPUT 127.0.0.1:9080/apisix/admin/routes/1 -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -d '{
"uri":"/anything/*",
"plugins": {
"openid-connect": {
"client_id": "myclient",
"client_secret": "e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq",
"discovery": "http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration",
"scope": "openid profile",
"bearer_only": true,
"realm": "myrealm",
"redirect_uri": "http://127.0.0.1:9080/anything/callback",
"logout_path": "/anything/logout"
}
},
"upstream":{
"type":"roundrobin",
"nodes":{
"httpbin.org:80":1
}
}
}'

screenshot
Click to Preview

  1. 未携带 X-Access-Token 访问 Apache APISIX 时将返回 401 表明未经授权:

screenshot
Click to Preview

  1. 调用 Keycloak API 获取 AccessToken:
curl -XPOST "http://127.0.0.1:8080/realms/myrealm/protocol/openid-connect/token" -d "grant_type=password&username=myuser&client_id=myclient&client_secret=e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq&password=mypassword"

screenshot
Click to Preview

  1. 将 AccessToken 放于 Authorization 头中请求 APISIX(替换掉 ${AccessToken}),可以认证成功:
curl http://127.0.0.1:9080/anything/test -H "Authorization: Bearer ${AccessToken}"

screenshot
Click to Preview

场景三:上游服务解析 UserInfo 信息

当启用 APISIX set_userinfo_header 配置后,认证成功后回调请求将携带 X-Userinfo 请求头,它包含了 User 的基本信息,可通过 base64_decode 获得用户内容。

常见问题

  1. 为什么 APISIX 中 Cookie 值非常长?

因为 APISIX 会将 id_tokenaccess_tokenrefresh_token 等值写入 Cookie 中,因此整个 Cookie 内容比较长。具体实现可阅读 lua-resty-openidc 库中设置 session 的逻辑。

  1. 如何修改 Session 存储的 Cookie 名称、存储位置?

目前 openid-connect 插件未提供自定义这部分配置的能力,因此可以使用 lua-resty-session 中提供的方法:通过 NGINX 变量的方式对其默认配置进行覆盖。 此处借助 APISIX 提供的 NGINX 配置注入能力以实现覆盖:通过在配置文件 {apisix}/conf/config.yaml 中添加这些代码,可修改 Session 存储 Cookie 的名称:

nginx_config:
http_server_configuration_snippet: |
set $session_name "session_override";

更多配置项可参考:

  1. lua-resty-session Init phase
  2. lua-resty-session Pluggable Storage Adapters

参考资料

  1. Integrate Keycloak with APISIX
  2. Keycloak Getting Started
  3. Keycloak Realm vs Client
  4. Keycloak Client vs User
  5. What is Client Scope?