Keycloak Node.js 适配器

Node.js 适配器,用于保护服务器端 JavaScript 应用

Keycloak 提供了一个基于 Connect 构建的 Node.js 适配器,用于保护服务器端 JavaScript 应用 - 其目标是足够灵活,可以与 Express.js 等框架集成。该适配器在底层使用 OpenID Connect 协议。您可以查看使用 OpenID Connect 保护应用程序和服务指南,了解有关 OpenID Connect 端点和功能的更通用信息。

该库可以直接从 Keycloak 组织下载,源代码可在 GitHub 上获取。

要使用 Node.js 适配器,首先您必须在 Keycloak 管理控制台中为您的应用程序创建一个客户端。该适配器支持公共、保密和仅持有者访问类型。选择哪种类型取决于用例场景。

创建客户端后,单击右上角的操作,然后选择下载适配器配置。对于格式,选择 *Keycloak OIDC JSON,然后单击下载。下载的 keycloak.json 文件位于您项目的根文件夹中。

安装

假设您已经安装了 Node.js,为您的应用程序创建一个文件夹

mkdir myapp && cd myapp

使用 npm init 命令为您的应用程序创建一个 package.json。现在将 Keycloak 连接适配器添加到依赖项列表中

    "dependencies": {
        "keycloak-connect": "26.1.1"
    }

用法

实例化 Keycloak 类

Keycloak 类为配置和与您的应用程序集成提供了一个中心点。最简单的创建方式不涉及任何参数。

在您的项目根目录中创建一个名为 server.js 的文件,并添加以下代码

    const session = require('express-session');
    const Keycloak = require('keycloak-connect');

    const memoryStore = new session.MemoryStore();
    const keycloak = new Keycloak({ store: memoryStore });

安装 express-session 依赖项

    npm install express-session

要启动 server.js 脚本,请在 package.json 的 'scripts' 部分添加以下命令

    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node server.js"
    },

现在我们可以使用以下命令运行我们的服务器

    npm run start

默认情况下,这将找到一个名为 keycloak.json 的文件,该文件与您应用程序的主可执行文件位于同一位置,在我们的例子中是根文件夹,用于初始化 Keycloak 特定设置,例如公钥、领域名称、各种 URL。

在这种情况下,需要部署 Keycloak 才能访问 Keycloak 管理控制台。

请访问有关如何使用 PodmanDocker 部署 Keycloak 管理控制台的链接

现在我们准备好通过访问 Keycloak 管理控制台 → 客户端(左侧边栏)→ 选择您的客户端 → 安装 → 格式选项 → Keycloak OIDC JSON → 下载来获取 keycloak.json 文件

将下载的文件粘贴到我们项目的根文件夹中。

使用此方法实例化将导致使用所有合理的默认值。作为替代方案,也可以提供配置对象,而不是 keycloak.json 文件

    const kcConfig = {
        clientId: 'myclient',
        bearerOnly: true,
        serverUrl: 'https://127.0.0.1:8080{kc_base_path}',
        realm: 'myrealm',
        realmPublicKey: 'MIIBIjANB...'
    };

    const keycloak = new Keycloak({ store: memoryStore }, kcConfig);

应用程序还可以通过使用以下代码将用户重定向到他们首选的身份提供商

    const keycloak = new Keycloak({ store: memoryStore, idpHint: myIdP }, kcConfig);
配置 Web 会话存储

如果您想使用 Web 会话来管理服务器端身份验证状态,您需要使用至少一个 store 参数初始化 Keycloak(…​),传入 express-session 正在使用的实际会话存储。

    const session = require('express-session');
    const memoryStore = new session.MemoryStore();

    // Configure session
    app.use(
      session({
        secret: 'mySecret',
        resave: false,
        saveUninitialized: true,
        store: memoryStore,
      })
    );

    const keycloak = new Keycloak({ store: memoryStore });
传递自定义 scope 值

默认情况下,scope 值 openid 作为查询参数传递给 Keycloak 的登录 URL,但您可以添加额外的自定义值

    const keycloak = new Keycloak({ scope: 'offline_access' });

安装中间件

实例化后,将中间件安装到您的 connect-capable 应用程序中

为了做到这一点,首先我们必须安装 Express

    npm install express

然后在我们的项目中需要 Express,如下所示

    const express = require('express');
    const app = express();

并在 Express 中配置 Keycloak 中间件,通过在下面的代码中添加

    app.use( keycloak.middleware() );

最后但并非最不重要的一点是,让我们设置我们的服务器监听 3000 端口上的 HTTP 请求,通过将以下代码添加到 main.js

    app.listen(3000, function () {
        console.log('App listening on port 3000');
    });

代理配置

如果应用程序在终止 SSL 连接的代理后面运行,则必须按照 express behind proxies 指南配置 Express。使用不正确的代理配置可能会导致生成无效的重定向 URI。

配置示例

    const app = express();

    app.set( 'trust proxy', true );

    app.use( keycloak.middleware() );

保护资源

简单身份验证

要强制用户必须经过身份验证才能访问资源,只需使用 keycloak.protect() 的无参数版本

    app.get( '/complain', keycloak.protect(), complaintHandler );
基于角色的授权

要使用当前应用程序的角色保护资源

    app.get( '/special', keycloak.protect('special'), specialHandler );

要使用不同应用程序的角色保护资源

    app.get( '/extra-special', keycloak.protect('other-app:special'), extraSpecialHandler );

要使用领域角色保护资源

    app.get( '/admin', keycloak.protect( 'realm:admin' ), adminHandler );
基于资源的授权

基于资源的授权允许您根据 Keycloak 中定义的一组策略来保护资源及其特定的方法/操作**,从而将授权从您的应用程序中外部化。这是通过公开一个 keycloak.enforcer 方法来实现的,您可以使用该方法来保护资源。*

    app.get('/apis/me', keycloak.enforcer('user:profile'), userProfileHandler);

keycloak-enforcer 方法以两种模式运行,具体取决于 response_mode 配置选项的值。

    app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'token'}), userProfileHandler);

如果 response_mode 设置为 token,则代表发送到您的应用程序的持有者令牌所代表的主体从服务器获取权限。在这种情况下,Keycloak 会颁发一个新的访问令牌,其中包含服务器授予的权限。如果服务器没有响应带有预期权限的令牌,则请求被拒绝。当使用此模式时,您应该能够从请求中获取令牌,如下所示

    app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'token'}), function (req, res) {
        const token = req.kauth.grant.access_token.content;
        const permissions = token.authorization ? token.authorization.permissions : undefined;

        // show user profile
    });

当您的应用程序使用会话并且您想要缓存来自服务器的先前决策时,以及自动处理刷新令牌时,首选此模式。此模式对于充当客户端和资源服务器的应用程序尤其有用。

如果 response_mode 设置为 permissions(默认模式),则服务器仅返回授予的权限列表,而不颁发新的访问令牌。除了不颁发新令牌外,此方法还通过 request 公开服务器授予的权限,如下所示

    app.get('/apis/me', keycloak.enforcer('user:profile', {response_mode: 'permissions'}), function (req, res) {
        const permissions = req.permissions;

        // show user profile
    });

无论使用哪种 response_modekeycloak.enforcer 方法都将首先尝试检查发送到您的应用程序的持有者令牌中的权限。如果持有者令牌已经携带了预期的权限,则无需与服务器交互以获得决策。当您的客户端能够在访问受保护资源之前从服务器获取具有预期权限的访问令牌时,这尤其有用,因此他们可以使用 Keycloak 授权服务提供的一些功能(例如增量授权),并避免在 keycloak.enforcer 强制访问资源时向服务器发出额外请求。

默认情况下,策略执行器将使用为应用程序定义的 client_id(例如,通过 keycloak.json)来引用 Keycloak 中支持 Keycloak 授权服务的客户端。在这种情况下,客户端不能是公共客户端,因为它实际上是一个资源服务器。

如果您的应用程序同时充当公共客户端(前端)和资源服务器(后端),您可以使用以下配置来引用 Keycloak 中具有您要强制执行的策略的不同客户端

      keycloak.enforcer('user:profile', {resource_server_id: 'my-apiserver'})

建议在 Keycloak 中使用不同的客户端来表示您的前端和后端。

如果您正在保护的应用程序启用了 Keycloak 授权服务,并且您在 keycloak.json 中定义了客户端凭据,您可以将其他声明推送到服务器,并使它们可用于您的策略,以便做出决策。为此,您可以定义一个 claims 配置选项,该选项期望一个 function,该函数返回一个 JSON,其中包含您要推送的声明

      app.get('/protected/resource', keycloak.enforcer(['resource:view', 'resource:write'], {
          claims: function(request) {
            return {
              "http.uri": ["/protected/resource"],
              "user.agent": // get user agent  from request
            }
          }
        }), function (req, res) {
          // access granted

有关如何配置 Keycloak 以保护您的应用程序资源的更多详细信息,请查看 授权服务指南

高级授权

要根据 URL 本身的部分来保护资源,假设每个部分都存在一个角色

    function protectBySection(token, request) {
      return token.hasRole( request.params.section );
    }

    app.get( '/:section/:page', keycloak.protect( protectBySection ), sectionHandler );

高级登录配置

默认情况下,所有未经授权的请求都将被重定向到 Keycloak 登录页面,除非您的客户端是仅持有者客户端。但是,保密客户端或公共客户端可以同时托管可浏览的端点和 API 端点。为了防止未经身份验证的 API 请求重定向,而是返回 HTTP 401,您可以覆盖 redirectToLogin 函数。

例如,此覆盖检查 URL 是否包含 /api/ 并禁用登录重定向

    Keycloak.prototype.redirectToLogin = function(req) {
    const apiReqMatcher = /\/api\//i;
    return !apiReqMatcher.test(req.originalUrl || req.url);
    };

其他 URL

显式用户触发的注销

默认情况下,中间件捕获对 /logout 的调用,以使用户通过以 Keycloak 为中心的注销工作流程。可以通过为 middleware() 调用指定 logout 配置参数来更改此行为

    app.use( keycloak.middleware( { logout: '/logoff' } ));

当用户触发注销时,可以传递查询参数 redirect_url

https://example.com/logoff?redirect_url=https%3A%2F%2Fexample.com%3A3000%2Flogged%2Fout

然后,此参数用作 OIDC 注销端点的重定向 url,用户将被重定向到 https://example.com/logged/out

Keycloak 管理回调

此外,中间件支持来自 Keycloak 控制台的回调,以注销单个会话或所有会话。默认情况下,这些类型的管理回调相对于 / 的根 URL 发生,但可以通过为 middleware() 调用提供 admin 参数来更改

    app.use( keycloak.middleware( { admin: '/callbacks' } );

完整示例

可以在 Node.js 的 Keycloak 快速入门中找到使用 Node.js 适配器的完整示例

升级 Node.js 适配器

要升级已复制到您的 Web 应用程序的 Node.js 适配器,请执行以下步骤。

步骤
  1. 下载新的适配器存档。

  2. 删除现有的 Node.js 适配器目录

  3. 将更新后的文件解压缩到其位置

  4. 更改应用程序 package.json 中 keycloak-connect 的依赖项

在此页上