11 12 25
Сделал закрытие сесси сервером если пароль не верный
This commit is contained in:
parent
7f91c60d26
commit
dbf1f22bac
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- обновляем контекст ---
|
// --- обновляем контекст ---
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user