通过 openresty 使用 keycloak 对 websocket 进行身份验证

Posted

技术标签:

【中文标题】通过 openresty 使用 keycloak 对 websocket 进行身份验证【英文标题】:Authenticate websocket with keycloak through openresty 【发布时间】:2019-07-28 09:28:31 【问题描述】:

目前我有一个包含以下组件的有效解决方案:

带有自定义应用程序的网络服务器 使用 lua 的 Openresty 钥匙斗篷

这允许我使用 keycloak 进行身份验证。 因为我的网络服务器还公开了一个 websocket 主机,所以我也想对这些 websocket 进行身份验证。有没有人有一个示例(nginx 文件作为 lua 文件)可用于使用 openresty 验证 websocket 连接?我已经查看了https://github.com/openresty/lua-resty-websocket,但似乎无法在身份验证部分找到插件的位置。 一个测试这个的示例客户端应用程序也很棒!

【问题讨论】:

【参考方案1】:

我自己想通了。在此处发布我的解决方案以帮助其他人实现相同的目标。 我有以下代码 sn-ps:

Openresty 配置

仅适用于 websocket,应放在服务器部分:

set $resty_user 'not_authenticated_resty_user';
location /ws 
      access_by_lua_file         /usr/local/openresty/nginx/conf/lua_access.lua;
      proxy_pass                    http://<backend-websocket-host>/ws;
      proxy_http_version            1.1;
      proxy_set_header              Host                $http_host;
      proxy_set_header              X-Real-IP           $remote_addr;
      proxy_set_header              X-Forwarded-For     $proxy_add_x_forwarded_for;

      proxy_set_header              Upgrade             $http_upgrade;
      proxy_set_header              Connection          "upgrade";
      proxy_set_header              X-Forwared-User     $resty_user;
      proxy_read_timeout            1d;
      proxy_send_timeout            1d;
    

lua_acces.lua

local opts = 
    redirect_uri = "/*",
    discovery = "http://<keycloak-url>/auth/realms/realm/.well-known/openid-configuration",
    client_id = "<client-id>",
    client_secret = "<client-secret>",
    redirect_uri_scheme = "https",
    logout_path = "/logout",
    redirect_after_logout_uri = "http://<keycloak-url>/auth/realms/realm/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2google.com",
    redirect_after_logout_with_id_token_hint = false,
    session_contents = id_token=true,
    ssl_verify=no
  

  -- call introspect for OAuth 2.0 Bearer Access Token validation
  local res, err = require("resty.openidc").bearer_jwt_verify(opts)
  if err or not res then
    print("Token authentication not succeeded")
    if err then
      print("jwt_verify error message:")
      print(err)
    end
    if res then
      print("jwt_verify response:")
      tprint(res)
    end
    res, err = require("resty.openidc").authenticate(opts)
    if err then
      ngx.status = 403
      ngx.say(err)
      ngx.exit(ngx.HTTP_FORBIDDEN)
    end
  end

if res.id_token and res.id_token.preferred_username then
    ngx.var.resty_user = res.id_token.preferred_username
  else
    ngx.var.resty_user = res.preferred_username
  end

这仅在 websocket 连接从 keycloak 服务中检索到有效令牌时才允许。 最后填写resty用户,将认证用户传递给后端应用。

示例 Java 客户端应用程序

获取密钥斗篷令牌

package test;

import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.AccessTokenResponse;

public class KeycloakConnection 
    private Keycloak _keycloak;

    public KeycloakConnection(final String host, String username, String password, String clientSecret, String realm, String clientId) 

        _keycloak = Keycloak.getInstance(
                "http://" + host + "/auth",
                realm,
                username,
                password,
                clientId,
                clientSecret);
    

    public String GetAccessToken()
    
        final AccessTokenResponse accessToken = _keycloak.tokenManager().getAccessToken();
        return accessToken.getToken();
    

网络套接字

这个 sn-p 只包含我调用来设置 websocket 连接的函数。您仍然需要实例化 _keycloakConnection 对象,在我的情况下,我有一个通用的 _session 字段,可以在每次需要时重复使用会话。

private Session GetWebsocketSession(String host)
    
        URI uri = URI.create("wss://" + host);
        ClientUpgradeRequest request = new ClientUpgradeRequest();
        request.setHeader("Authorization", "Bearer " + _keycloakConnection.GetAccessToken());
        _client = new WebSocketClient();
        try 
                _client.start();
                // The socket that receives events
                WebsocketEventHandler socketEventHandler = new WebsocketEventHandler(this::NewLiveMessageReceivedInternal);
                // Attempt Connect
                Future<Session> fut = _client.connect(socketEventHandler, uri, request);
                // Wait for Connect
                _session = fut.get();

                return _session;
         catch (Throwable t) 
            _logger.error("Error during websocket session creation", t);
        
        return null;
    

WebsocketEventHandler

在这个类中注入一个消费者来消费另一个类中的消息

package test;

import org.apache.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;

import java.util.function.Consumer;

public class WebsocketEventHandler extends WebSocketAdapter

    private final Logger _logger;
    private Consumer<String> _onMessage;

    public WebsocketEventHandler(Consumer<String> onMessage) 
        _onMessage = onMessage;
        _logger = Logger.getLogger(WebsocketEventHandler.class);
    

    @Override
    public void onWebSocketConnect(Session sess)
    
        super.onWebSocketConnect(sess);
        _logger.info("Socket Connected: " + sess);
    

    @Override
    public void onWebSocketText(String message)
    
        super.onWebSocketText(message);
        _logger.info("Received TEXT message: " + message);
        _onMessage.accept(message);
    

    @Override
    public void onWebSocketClose(int statusCode, String reason)
    
        super.onWebSocketClose(statusCode,reason);
        _logger.info("Socket Closed: [" + statusCode + "] " + reason);
    

    @Override
    public void onWebSocketError(Throwable cause)
    
        super.onWebSocketError(cause);
        _logger.error("Websocket error", cause);
    

发送消息

创建 _session 后,您可以使用以下行发送数据:

_session.getRemote().sendString("Hello world");

这些 sn-ps 都是我整个解决方案的一小部分。我可能错过了什么。如果有人有问题或这不适用于您的情况,请联系我们,我会提供更多信息。

【讨论】:

以上是关于通过 openresty 使用 keycloak 对 websocket 进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章

Openresty日志输出请求头 log lua

通过 Keycloak 实现 Grafana 单点登录

使用 cUrl 通过其 REST API 配置 Keycloak

使用 JAVA 通过 REST API 集成 Keycloak 时在 toRepresentation() 处获取 404

通过keycloak认证postgrest api

如何通过employeeNumber或自定义属性搜索keycloak用户?