From dbf1f22bac2f564a470ada9601ae1498a01fec3bd63660de31fdebfc68da9f07 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Thu, 11 Dec 2025 16:03:46 +0300 Subject: [PATCH] =?UTF-8?q?11=2012=2025=20=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D0=B5=D1=81=D1=81=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=BE=D0=BB=D1=8C=20=D0=BD=D0=B5=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ws_protocol/JSON/ConnectionContext.java | 14 +++-- .../auth/Net_AuthChallenge_Handler.java | 11 +++- .../auth/Net_CreateAuthSession__Handler.java | 54 +++++++++++++----- .../java/server/ws/WsConnectionUtils.java | 56 +++++++++++++++++++ 4 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 shine-server-net-protocol/src/main/java/server/ws/WsConnectionUtils.java diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index dac86b3..d655969 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -11,8 +11,9 @@ import shine.db.entities.ActiveSession; public class ConnectionContext { // Статусы аутентификации - public static final int AUTH_STATUS_NONE = 0; // анонимный или не авторизованный пользователь - public static final int AUTH_STATUS_USER = 1; // авторизованный пользователь + public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован + public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge, ждём CreateAuthSession + public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь // Полный пользователь из БД (solana_users) private SolanaUser solanaUser; @@ -26,10 +27,15 @@ public class ConnectionContext { private String sessionId; /** - * Временный секрет шага 1, который используется на шаге 2 и хранится в БД. + * Временный секрет шага 1, который используется на шаге 2 и хранится в БД, + * а после успешной авторизации — настоящий секрет сессии. */ private String sessionPwd; + /** + * Текущий статус аутентификации. + * См. константы AUTH_STATUS_* + */ private int authenticationStatus = AUTH_STATUS_NONE; /** @@ -132,4 +138,4 @@ public class ConnectionContext { ", authenticationStatus=" + authenticationStatus + '}'; } -} +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java index eeffa0b..4486a0b 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java @@ -13,6 +13,12 @@ import shine.db.entities.SolanaUser; import java.security.SecureRandom; import java.util.Base64; +/** + * Шаг 1 авторизации: запрос выдачи временного nonce (authNonce). + * + * Клиент по логину просит сервер сгенерировать случайный authNonce, + * который будет использован на втором шаге при подписи. + */ public class Net_AuthChallenge_Handler implements JsonMessageHandler { private static final SecureRandom RANDOM = new SecureRandom(); @@ -32,7 +38,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { ); } - // 1) Проверка: в контексте никто не авторизован + // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию if (ctx.getLogin() != null) { return NetExceptionResponseFactory.error( req, @@ -57,6 +63,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { // 3) Заполняем контекст пользователем ctx.setSolanaUser(solanaUser); + // 3.1) Отмечаем, что по этому соединению начата авторификация + ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS); + // 4) Генерируем одноразовый authNonce = base64(32 случайных байт) byte[] buf = new byte[32]; RANDOM.nextBytes(buf); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index a5ccca4..08eecc4 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.eclipse.jetty.websocket.api.Session; import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry; import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Response; @@ -11,6 +12,7 @@ import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; +import server.ws.WsConnectionUtils; import shine.db.dao.ActiveSessionsDAO; import shine.db.entities.ActiveSession; import shine.db.entities.SolanaUser; @@ -18,8 +20,6 @@ import shine.geo.ClientInfoService; import shine.geo.GeoLookupService; import utils.crypto.Ed25519Util; -import org.eclipse.jetty.websocket.api.Session; - import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.security.SecureRandom; @@ -44,6 +44,9 @@ import java.util.Base64; * - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время; * - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage; * - возвращается sessionId и sessionPwd в ответе. + * + * При ошибке авторификации (битые данные, подпись, время и т.п.) — + * соединение закрывается через WsConnectionUtils. */ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { @@ -58,52 +61,63 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { // --- базовые проверки контекста --- if (ctx == null || ctx.getSolanaUser() == null || ctx.getSessionPwd() == null) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "NO_STEP1_CONTEXT", "Шаг 1 авторизации не был корректно выполнен для данного соединения" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context"); + return err; } - if (!ctx.isAnonymous()) { - return NetExceptionResponseFactory.error( + // Ожидаем, что перед этим был AuthChallenge и статус = AUTH_IN_PROGRESS + if (ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) { + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, - "ALREADY_AUTHED", - "Пользователь уже авторизован по текущему соединению" + "BAD_AUTH_FLOW_STATE", + "Неожиданное состояние авторификации для данного соединения" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad auth flow state"); + return err; } SolanaUser user = ctx.getSolanaUser(); Long loginId = user.getLoginId(); if (loginId == null) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, "NO_LOGIN_ID", "Для пользователя не задан loginId в БД" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no loginId"); + return err; } String storagePwd = req.getStoragePwd(); if (storagePwd == null || storagePwd.isBlank()) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "EMPTY_STORAGE_PWD", "Пустой storagePwd" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd"); + return err; } String signatureB64 = req.getSignatureB64(); if (signatureB64 == null || signatureB64.isBlank()) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "EMPTY_SIGNATURE", "Пустая цифровая подпись" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature"); + return err; } long timeMs = req.getTimeMs(); @@ -111,12 +125,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { long diff = Math.abs(nowMs - timeMs); if (diff > ALLOWED_SKEW_MS) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "TIME_SKEW", "Время клиента отличается от сервера более чем на 30 секунд" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew"); + return err; } // Короткая строка clientInfo от клиента (до 50 символов) @@ -128,12 +144,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { // --- выбираем публичный ключ pubkey1 --- String pubKeyB64 = user.getDeviceKey(); if (pubKeyB64 == null || pubKeyB64.isBlank()) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "NO_PUBKEY1", "Отсутствует публичный ключ pubkey1 для пользователя" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no pubkey"); + return err; } byte[] publicKey32; @@ -142,12 +160,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64); signature64 = Base64.getDecoder().decode(signatureB64); } catch (IllegalArgumentException ex) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "Некорректный формат Base64 для ключа или подписи" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64"); + return err; } // --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 --- @@ -159,12 +179,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); if (!sigOk) { - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature"); + return err; } // --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId --- @@ -222,12 +244,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { dao.insert(activeSession); } catch (SQLException e) { log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); - return NetExceptionResponseFactory.error( + Net_Response err = NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, "DB_ERROR_SESSION_CREATE", "Ошибка БД при создании сессии" ); + WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error"); + return err; } // --- обновляем контекст --- diff --git a/shine-server-net-protocol/src/main/java/server/ws/WsConnectionUtils.java b/shine-server-net-protocol/src/main/java/server/ws/WsConnectionUtils.java new file mode 100644 index 0000000..8fbe79e --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/ws/WsConnectionUtils.java @@ -0,0 +1,56 @@ +package server.ws; + +import org.eclipse.jetty.websocket.api.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry; +import server.logic.ws_protocol.JSON.ConnectionContext; + +/** + * Утилита для работы с WebSocket-подключениями. + */ +public final class WsConnectionUtils { + + private static final Logger log = LoggerFactory.getLogger(WsConnectionUtils.class); + + private WsConnectionUtils() { + // utility + } + + /** + * Корректно закрывает WebSocket-соединение: + * - удаляет контекст из ActiveConnectionsRegistry; + * - очищает ConnectionContext; + * - закрывает сам WebSocket (если ещё открыт). + * + * @param ctx контекст соединения + * @param statusCode код закрытия WebSocket (например, 1000, 4001) + * @param reason причина закрытия (для логов/клиента) + */ + public static void closeConnection(ConnectionContext ctx, int statusCode, String reason) { + if (ctx == null) { + return; + } + + Session ws = ctx.getWsSession(); + + try { + // Удаляем контекст из реестра активных соединений + ActiveConnectionsRegistry.getInstance().remove(ctx); + + // Чистим контекст + ctx.reset(); + + // Закрываем WebSocket-сессию + if (ws != null && ws.isOpen()) { + try { + ws.close(statusCode, reason); + } catch (Exception e) { + log.warn("Не удалось закрыть WebSocket-сессию (statusCode={}, reason={})", statusCode, reason, e); + } + } + } catch (Exception e) { + log.warn("Ошибка при закрытии WebSocket-соединения", e); + } + } +} \ No newline at end of file