Аутентифицировать websocket с помощью keycloak через openresty
В настоящее время у меня есть рабочее решение со следующими компонентами:
- Веб-сервер с пользовательским приложением
- Опенресты с Луа
- Keycloak
Это позволяет мне проходить аутентификацию с помощью Keycloak.
Поскольку мой веб-сервер также предоставляет хост веб-сокетов, я хотел бы также аутентифицировать эти веб-сокеты. У кого-нибудь есть пример (и файл nginx, и файл lua), доступный для аутентификации соединений через веб-сокеты с использованием openresty? Я посмотрел на https://github.com/openresty/lua-resty-websocket но не могу найти, где подключить в части аутентификации.
Пример клиентского приложения, чтобы проверить это также было бы здорово!
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
Получить токен Keycloak
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();
}
}
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");
Эти фрагменты - небольшая часть всего моего решения. Я мог что-то пропустить. Если у кого-то есть вопрос или он не работает в вашем случае, пожалуйста, свяжитесь с нами, и я предоставлю дополнительную информацию.