Сделал закрытие сесси сервером если пароль не верный
This commit is contained in:
AidarKC 2025-12-11 16:03:46 +03:00
parent 7f91c60d26
commit dbf1f22bac
4 changed files with 115 additions and 20 deletions

View File

@ -11,8 +11,9 @@ import shine.db.entities.ActiveSession;
public class ConnectionContext { public class ConnectionContext {
// Статусы аутентификации // Статусы аутентификации
public static final int AUTH_STATUS_NONE = 0; // анонимный или не авторизованный пользователь public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
public static final int AUTH_STATUS_USER = 1; // авторизованный пользователь public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge, ждём CreateAuthSession
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
// Полный пользователь из БД (solana_users) // Полный пользователь из БД (solana_users)
private SolanaUser solanaUser; private SolanaUser solanaUser;
@ -26,10 +27,15 @@ public class ConnectionContext {
private String sessionId; private String sessionId;
/** /**
* Временный секрет шага 1, который используется на шаге 2 и хранится в БД. * Временный секрет шага 1, который используется на шаге 2 и хранится в БД,
* а после успешной авторизации настоящий секрет сессии.
*/ */
private String sessionPwd; private String sessionPwd;
/**
* Текущий статус аутентификации.
* См. константы AUTH_STATUS_*
*/
private int authenticationStatus = AUTH_STATUS_NONE; private int authenticationStatus = AUTH_STATUS_NONE;
/** /**

View File

@ -13,6 +13,12 @@ import shine.db.entities.SolanaUser;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Base64; import java.util.Base64;
/**
* Шаг 1 авторизации: запрос выдачи временного nonce (authNonce).
*
* Клиент по логину просит сервер сгенерировать случайный authNonce,
* который будет использован на втором шаге при подписи.
*/
public class Net_AuthChallenge_Handler implements JsonMessageHandler { public class Net_AuthChallenge_Handler implements JsonMessageHandler {
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
@ -32,7 +38,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
); );
} }
// 1) Проверка: в контексте никто не авторизован // Если по этому соединению уже есть залогиненный пользователь не даём повторную авторификацию
if (ctx.getLogin() != null) { if (ctx.getLogin() != null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
@ -57,6 +63,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
// 3) Заполняем контекст пользователем // 3) Заполняем контекст пользователем
ctx.setSolanaUser(solanaUser); ctx.setSolanaUser(solanaUser);
// 3.1) Отмечаем, что по этому соединению начата авторификация
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
// 4) Генерируем одноразовый authNonce = base64(32 случайных байт) // 4) Генерируем одноразовый authNonce = base64(32 случайных байт)
byte[] buf = new byte[32]; byte[] buf = new byte[32];
RANDOM.nextBytes(buf); RANDOM.nextBytes(buf);

View File

@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.eclipse.jetty.websocket.api.Session;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry; import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Response; 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.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes; import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUser;
@ -18,8 +20,6 @@ import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService; import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util; import utils.crypto.Ed25519Util;
import org.eclipse.jetty.websocket.api.Session;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.sql.SQLException; import java.sql.SQLException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -44,6 +44,9 @@ import java.util.Base64;
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время; * - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
* - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage; * - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage;
* - возвращается sessionId и sessionPwd в ответе. * - возвращается sessionId и sessionPwd в ответе.
*
* При ошибке авторификации (битые данные, подпись, время и т.п.)
* соединение закрывается через WsConnectionUtils.
*/ */
public class Net_CreateAuthSession__Handler implements JsonMessageHandler { 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) { if (ctx == null || ctx.getSolanaUser() == null || ctx.getSessionPwd() == null) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"NO_STEP1_CONTEXT", "NO_STEP1_CONTEXT",
"Шаг 1 авторизации не был корректно выполнен для данного соединения" "Шаг 1 авторизации не был корректно выполнен для данного соединения"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context");
return err;
} }
if (!ctx.isAnonymous()) { // Ожидаем, что перед этим был AuthChallenge и статус = AUTH_IN_PROGRESS
return NetExceptionResponseFactory.error( if (ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, 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(); SolanaUser user = ctx.getSolanaUser();
Long loginId = user.getLoginId(); Long loginId = user.getLoginId();
if (loginId == null) { if (loginId == null) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
"NO_LOGIN_ID", "NO_LOGIN_ID",
"Для пользователя не задан loginId в БД" "Для пользователя не задан loginId в БД"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no loginId");
return err;
} }
String storagePwd = req.getStoragePwd(); String storagePwd = req.getStoragePwd();
if (storagePwd == null || storagePwd.isBlank()) { if (storagePwd == null || storagePwd.isBlank()) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"EMPTY_STORAGE_PWD", "EMPTY_STORAGE_PWD",
"Пустой storagePwd" "Пустой storagePwd"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
return err;
} }
String signatureB64 = req.getSignatureB64(); String signatureB64 = req.getSignatureB64();
if (signatureB64 == null || signatureB64.isBlank()) { if (signatureB64 == null || signatureB64.isBlank()) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"EMPTY_SIGNATURE", "EMPTY_SIGNATURE",
"Пустая цифровая подпись" "Пустая цифровая подпись"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
return err;
} }
long timeMs = req.getTimeMs(); long timeMs = req.getTimeMs();
@ -111,12 +125,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
long diff = Math.abs(nowMs - timeMs); long diff = Math.abs(nowMs - timeMs);
if (diff > ALLOWED_SKEW_MS) { if (diff > ALLOWED_SKEW_MS) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"TIME_SKEW", "TIME_SKEW",
"Время клиента отличается от сервера более чем на 30 секунд" "Время клиента отличается от сервера более чем на 30 секунд"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
return err;
} }
// Короткая строка clientInfo от клиента (до 50 символов) // Короткая строка clientInfo от клиента (до 50 символов)
@ -128,12 +144,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
// --- выбираем публичный ключ pubkey1 --- // --- выбираем публичный ключ pubkey1 ---
String pubKeyB64 = user.getDeviceKey(); String pubKeyB64 = user.getDeviceKey();
if (pubKeyB64 == null || pubKeyB64.isBlank()) { if (pubKeyB64 == null || pubKeyB64.isBlank()) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"NO_PUBKEY1", "NO_PUBKEY1",
"Отсутствует публичный ключ pubkey1 для пользователя" "Отсутствует публичный ключ pubkey1 для пользователя"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no pubkey");
return err;
} }
byte[] publicKey32; byte[] publicKey32;
@ -142,12 +160,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64); publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
signature64 = Base64.getDecoder().decode(signatureB64); signature64 = Base64.getDecoder().decode(signatureB64);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_BASE64", "BAD_BASE64",
"Некорректный формат Base64 для ключа или подписи" "Некорректный формат Base64 для ключа или подписи"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
return err;
} }
// --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 --- // --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 ---
@ -159,12 +179,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
if (!sigOk) { if (!sigOk) {
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.UNVERIFIED,
"BAD_SIGNATURE", "BAD_SIGNATURE",
"Подпись не прошла проверку" "Подпись не прошла проверку"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
return err;
} }
// --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId --- // --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId ---
@ -222,12 +244,14 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
dao.insert(activeSession); dao.insert(activeSession);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e);
return NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
"DB_ERROR_SESSION_CREATE", "DB_ERROR_SESSION_CREATE",
"Ошибка БД при создании сессии" "Ошибка БД при создании сессии"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
return err;
} }
// --- обновляем контекст --- // --- обновляем контекст ---

View File

@ -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);
}
}
}