Аутентифицировать веб-сокет с помощью keycloak через openresty

В настоящее время у меня есть рабочее решение со следующими компонентами:

  • Веб-сервер с пользовательским приложением
  • Openresty с lua
  • Брелок

Это позволяет мне аутентифицироваться с помощью keycloak.
Поскольку мой веб-сервер также предоставляет хост веб-сокетов, я хотел бы также аутентифицировать эти веб-сокеты. Есть ли у кого-нибудь пример (как файл nginx, так и файл lua), доступный для аутентификации соединений через веб-сокет с использованием openresty? Я просмотрел https://github.com/openresty/lua-resty-websocket но, кажется, не могу найти, где подключить плагин в части аутентификации.
Пример клиентского приложения для проверки этого тоже был бы замечательным!


person Bob Claerhout    schedule 06.03.2019    source источник


Ответы (1)


Я сам разобрался. Публикую свое решение здесь, чтобы помочь другим достичь того же.
У меня есть следующие фрагменты кода:

Конфигурация OpenResty

только для веб-сокета, должен быть размещен внутри раздела сервера:

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

Это разрешает подключения через веб-сокет только в том случае, если у них есть действительный токен, полученный из службы keycloak.
В конце заполняется оставшийся пользователь, чтобы передать аутентифицированного пользователя серверному приложению.

Пример клиентского 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();
    }
}

Веб-сокет

Этот фрагмент содержит только функцию, которую я вызываю для настройки соединения через веб-сокет. Вам все еще нужно создать экземпляр объекта _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");

Эти фрагменты — небольшая часть всего моего решения. Я мог что-то упустить. Если у кого-то есть вопросы или это не работает в вашем случае, свяжитесь с нами, и я предоставлю дополнительную информацию.

person Bob Claerhout    schedule 08.03.2019