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

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

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

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

【问题讨论】:

    标签: nginx websocket keycloak openresty lua-resty-openidc


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

    【讨论】:

      猜你喜欢
      • 2022-10-07
      • 1970-01-01
      • 2019-01-30
      • 2021-09-25
      • 2017-09-18
      • 1970-01-01
      • 1970-01-01
      • 2021-04-26
      • 2019-03-02
      相关资源
      最近更新 更多