04 12 25 Версия авторификации где сервер выдовал сессион Ид
This commit is contained in:
parent
fc748a744c
commit
c9bfa2d01a
@ -16,9 +16,10 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.eclipse.jetty:jetty-server:11.0.20'
|
||||
implementation 'org.eclipse.jetty:jetty-server:11.0.20' // WS сервер
|
||||
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
|
||||
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
|
||||
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' // шифрование
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import java.net.http.HttpResponse;
|
||||
|
||||
/**
|
||||
* Сервис для геолокации по IP.
|
||||
*
|
||||
*.
|
||||
* Основной метод:
|
||||
* resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось.
|
||||
*/
|
||||
|
||||
@ -2,7 +2,7 @@ package shine.geo;
|
||||
|
||||
/**
|
||||
* Тестовый запуск геолокации.
|
||||
*
|
||||
*.
|
||||
* Логика:
|
||||
* 1) Если в args[0] передан IP — используем его.
|
||||
* 2) Иначе пробуем узнать внешний IP текущей машины.
|
||||
|
||||
@ -16,6 +16,10 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.eclipse.jetty:jetty-server:11.0.20' // WS сервер
|
||||
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
|
||||
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
|
||||
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json
|
||||
|
||||
implementation 'org.slf4j:slf4j-api:2.0.9'
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
package server.logic.ws_protocol.JSON;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* Реестр активных подключений (только авторизованные).
|
||||
*.
|
||||
* Позволяет:
|
||||
* - получить ConnectionContext по sessionId;
|
||||
* - получить все активные подключения пользователя по loginId;
|
||||
* - удалить подключение при закрытии WebSocket.
|
||||
*.
|
||||
* найти все подключения пользователя:
|
||||
* var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId);
|
||||
*.
|
||||
* найти конкретное подключение по sessionId:
|
||||
* ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
|
||||
* Session ws = ctx != null ? ctx.getWsSession() : null;
|
||||
*/
|
||||
|
||||
public final class ActiveConnectionsRegistry {
|
||||
|
||||
private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
|
||||
|
||||
public static ActiveConnectionsRegistry getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private ActiveConnectionsRegistry() {
|
||||
// singleton
|
||||
}
|
||||
|
||||
// sessionId -> ConnectionContext
|
||||
private final ConcurrentHashMap<Long, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
|
||||
|
||||
// loginId -> множество ConnectionContext для этого пользователя
|
||||
private final ConcurrentHashMap<Long, Set<ConnectionContext>> byLoginId = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Зарегистрировать авторизованное подключение.
|
||||
* Ожидается, что в ctx уже выставлены loginId и sessionId.
|
||||
*/
|
||||
public void register(ConnectionContext ctx) {
|
||||
if (ctx == null) return;
|
||||
|
||||
Long sessionId = ctx.getSessionId();
|
||||
Long loginId = ctx.getLoginId();
|
||||
|
||||
if (sessionId == null || loginId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bySessionId.put(sessionId, ctx);
|
||||
|
||||
byLoginId
|
||||
.computeIfAbsent(loginId, id -> new CopyOnWriteArraySet<>())
|
||||
.add(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить подключение по контексту (например, при onClose).
|
||||
*/
|
||||
public void remove(ConnectionContext ctx) {
|
||||
if (ctx == null) return;
|
||||
|
||||
Long sessionId = ctx.getSessionId();
|
||||
Long loginId = ctx.getLoginId();
|
||||
|
||||
if (sessionId != null) {
|
||||
bySessionId.remove(sessionId);
|
||||
}
|
||||
|
||||
if (loginId != null) {
|
||||
Set<ConnectionContext> set = byLoginId.get(loginId);
|
||||
if (set != null) {
|
||||
set.remove(ctx);
|
||||
if (set.isEmpty()) {
|
||||
byLoginId.remove(loginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить подключение по sessionId.
|
||||
*/
|
||||
public void removeBySessionId(long sessionId) {
|
||||
ConnectionContext ctx = bySessionId.remove(sessionId);
|
||||
if (ctx != null) {
|
||||
Long loginId = ctx.getLoginId();
|
||||
if (loginId != null) {
|
||||
Set<ConnectionContext> set = byLoginId.get(loginId);
|
||||
if (set != null) {
|
||||
set.remove(ctx);
|
||||
if (set.isEmpty()) {
|
||||
byLoginId.remove(loginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить контекст по sessionId.
|
||||
*/
|
||||
public ConnectionContext getBySessionId(long sessionId) {
|
||||
return bySessionId.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все активные подключения пользователя по loginId.
|
||||
*/
|
||||
public Set<ConnectionContext> getByLoginId(long loginId) {
|
||||
Set<ConnectionContext> set = byLoginId.get(loginId);
|
||||
if (set == null) {
|
||||
return Set.of();
|
||||
}
|
||||
// CopyOnWriteArraySet безопасно отдавать как есть
|
||||
return set;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package server.logic.ws_protocol.JSON;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import shine.db.entities.SolanaUser;
|
||||
import shine.db.entities.ActiveSession;
|
||||
|
||||
@ -24,6 +25,22 @@ public class ConnectionContext {
|
||||
|
||||
private int authenticationStatus = AUTH_STATUS_NONE;
|
||||
|
||||
/**
|
||||
* WebSocket-сессия Jetty для данного подключения.
|
||||
* Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
|
||||
*/
|
||||
private Session wsSession;
|
||||
|
||||
// --- WebSocket Session ---
|
||||
|
||||
public Session getWsSession() {
|
||||
return wsSession;
|
||||
}
|
||||
|
||||
public void setWsSession(Session wsSession) {
|
||||
this.wsSession = wsSession;
|
||||
}
|
||||
|
||||
// --- SolanaUser / ActiveSession ---
|
||||
|
||||
public SolanaUser getSolanaUser() {
|
||||
@ -96,6 +113,7 @@ public class ConnectionContext {
|
||||
sessionPwd = null;
|
||||
|
||||
authenticationStatus = AUTH_STATUS_NONE;
|
||||
wsSession = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -16,7 +16,7 @@ import java.util.Map;
|
||||
/**
|
||||
* JsonHandlerRegistry — единое место, где руками регистрируются
|
||||
* JSON-операции: op → handler и op → requestClass.
|
||||
*
|
||||
*.
|
||||
* Если нужно добавить новый запрос:
|
||||
* 1) создаёшь класс NetXXXRequest / NetXXXResponse,
|
||||
* 2) создаёшь JsonMessageHandler (NetXXXHandler),
|
||||
|
||||
@ -17,7 +17,7 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* JsonInboundProcessor — обработка JSON-сообщений.
|
||||
*
|
||||
*.
|
||||
* 1) Парсит общий пакет (op, requestId,...).
|
||||
* 2) По op выбирает класс запроса и хэндлер.
|
||||
* 3) Маппит JSON → NetRequest через ObjectMapper.
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
||||
|
||||
/**
|
||||
* Шаг 2 авторизации: клиент подтверждает владение ключом.
|
||||
*
|
||||
*.
|
||||
* JSON:
|
||||
* {
|
||||
* "op": "AuthSessionNewStep2",
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
||||
|
||||
/**
|
||||
* Ответ на AuthSessionNewStep2.
|
||||
*
|
||||
*.
|
||||
* Успешный JSON:
|
||||
* {
|
||||
* "op": "AuthSessionNewStep2",
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
||||
|
||||
/**
|
||||
* Запрос SessionRefresh.
|
||||
*
|
||||
*.
|
||||
* JSON (payload):
|
||||
* {
|
||||
* "sessionId": 123,
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
||||
|
||||
/**
|
||||
* Успешный ответ на SessionRefresh.
|
||||
*
|
||||
*.
|
||||
* Дополнительных полей нет, достаточно status=200 и (опционально) пустого payload.
|
||||
*/
|
||||
public class NetSessionRefreshResponse extends NetResponse {
|
||||
|
||||
@ -3,7 +3,7 @@ package server.logic.ws_protocol.JSON.entyties;
|
||||
/**
|
||||
* Базовый класс для всех событий (event).
|
||||
* Общие поля: op и payload.
|
||||
*
|
||||
*.
|
||||
* Формат JSON (event):
|
||||
* {
|
||||
* "op": "...",
|
||||
|
||||
@ -2,7 +2,7 @@ package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Ответ с ошибкой (любой отказ).
|
||||
*
|
||||
*.
|
||||
* В payload будет:
|
||||
* {
|
||||
* "code": "...",
|
||||
|
||||
@ -2,9 +2,9 @@ package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Базовый класс для всех запросов (client → server).
|
||||
*
|
||||
*.
|
||||
* Наследуется от NetEvent и добавляет requestId.
|
||||
*
|
||||
*.
|
||||
* Формат JSON (request):
|
||||
* {
|
||||
* "op": "...",
|
||||
|
||||
@ -2,9 +2,9 @@ package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Базовый класс для всех ответов (server → client).
|
||||
*
|
||||
*.
|
||||
* Наследуется от NetRequest и добавляет status.
|
||||
*
|
||||
*.
|
||||
* Формат JSON (response):
|
||||
* {
|
||||
* "op": "...",
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
||||
|
||||
/**
|
||||
* Запрос AddUser.
|
||||
*
|
||||
*.
|
||||
* Ожидаемый JSON:
|
||||
* {
|
||||
* "op": "AddUser",
|
||||
|
||||
@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
||||
import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
||||
@ -22,7 +23,7 @@ import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* Шаг 2 авторизации: проверка подписи и создание сессии.
|
||||
*
|
||||
*.
|
||||
* Клиент присылает:
|
||||
* - loginId
|
||||
* - sigNum (0 или 1)
|
||||
@ -165,6 +166,9 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
// Регистрируем это подключение в глобальном реестре активных соединений
|
||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||
|
||||
// --- формируем ответ ---
|
||||
NetAuthSessionNewStep2Response resp = new NetAuthSessionNewStep2Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
||||
import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
||||
@ -108,6 +109,9 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setSessionPwd(sessionPwd);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
// Регистрируем это подключение в глобальном реестре активных соединений
|
||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||
}
|
||||
|
||||
// И возвращаем OK без доп. полей (payload будет {}).
|
||||
|
||||
@ -2,11 +2,11 @@ package server.logic.ws_protocol;
|
||||
|
||||
/**
|
||||
* WireCodes — константы бинарного протокола поверх WebSocket.
|
||||
*
|
||||
*.
|
||||
* Формат входящего сообщения:
|
||||
* [4] int opCode (big-endian)
|
||||
* [*] payload
|
||||
*
|
||||
*.
|
||||
* Ответ сервера:
|
||||
* ровно [4] int statusCode (big-endian)
|
||||
*/
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
* ============================================================================
|
||||
* BchBlockEntry — универсальная запись блокчейна SHiNE (.bch)
|
||||
* ============================================================================
|
||||
*
|
||||
*.
|
||||
* 🧩 Формат файла .bch:
|
||||
* Каждый блок хранится последовательно, без промежутков.
|
||||
* Один блок = «заголовок» (RAW) + подпись (64) + хэш (32).
|
||||
*
|
||||
*.
|
||||
* FULL = RAW + signature(64) + hash(32)
|
||||
*
|
||||
*.
|
||||
* ---------------------------------------------------------------------------
|
||||
* 🔹 Структура RAW-части блока (без подписи и хэша)
|
||||
* ---------------------------------------------------------------------------
|
||||
* Размеры и порядок строго фиксированы (BigEndian).
|
||||
*
|
||||
*.
|
||||
* Порядок байтов (сверху вниз, смещения от начала RAW):
|
||||
*
|
||||
*.
|
||||
* ┌────────────────────────────┬────────┬───────────────────────────────┐
|
||||
* │ Поле │ Размер │ Описание │
|
||||
* ├────────────────────────────┼────────┼───────────────────────────────┤
|
||||
@ -30,22 +30,22 @@
|
||||
* │ recordTypeVersion │ 2 байта│ версия структуры данного типа │
|
||||
* │ body │ M байт │ бинарное тело записи │
|
||||
* └────────────────────────────┴────────┴───────────────────────────────┘
|
||||
*
|
||||
*.
|
||||
* ⇒ RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 2 = 20 байт.
|
||||
* ⇒ recordSize = RAW_HEADER_SIZE + body.length
|
||||
*
|
||||
*.
|
||||
* ---------------------------------------------------------------------------
|
||||
* 🔹 Структура FULL-блока
|
||||
* ---------------------------------------------------------------------------
|
||||
*
|
||||
*.
|
||||
* ┌────────────────────────────┬─────────┬──────────────────────────────┐
|
||||
* │ RAW │ M+20 │ тело блока без подписи │
|
||||
* │ signature64 │ 64 │ подпись Ed25519(preimage) │
|
||||
* │ hash32 │ 32 │ SHA-256(preimage) │
|
||||
* └────────────────────────────┴─────────┴──────────────────────────────┘
|
||||
*
|
||||
*.
|
||||
* ⇒ Общая длина FULL = recordSize + 96 байт.
|
||||
*
|
||||
*.
|
||||
* ---------------------------------------------------------------------------
|
||||
* 🔹 Канонический preimage для подписи/хэша
|
||||
* ---------------------------------------------------------------------------
|
||||
@ -58,9 +58,9 @@
|
||||
* можно номер блока?
|
||||
* prevHash32(32B) +
|
||||
* rawBytes (M+20B)
|
||||
*
|
||||
*.
|
||||
* hash32 = SHA-256(preimage)
|
||||
* signature64= Ed25519.sign(preimage, privateKey)
|
||||
*
|
||||
*.
|
||||
* Проверка осуществляется через {@link utils.crypto.BchCryptoVerifier}.
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ import java.util.Arrays;
|
||||
* AddBlockHandler — обработчик команды "добавить блок" (ADD_BLOCK)
|
||||
* ---------------------------------------------------------------
|
||||
* Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку.
|
||||
*
|
||||
*.
|
||||
* Формат входного сообщения (msg):
|
||||
* [0..3] — 4 байта: код операции (WireCodes.ADD_BLOCK)
|
||||
* [4..11] — 8 байт: blockchainId (уникальный идентификатор цепочки)
|
||||
@ -33,13 +33,13 @@ import java.util.Arrays;
|
||||
* ├── M байт body (содержимое блока)
|
||||
* ├── 64 байта signature (Ed25519)
|
||||
* └── 32 байта hash (SHA-256)
|
||||
*
|
||||
*.
|
||||
* ---------------------------------------------------------------
|
||||
* Алгоритм работы:
|
||||
*
|
||||
*.
|
||||
* 1️⃣ Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи).
|
||||
* 2️⃣ Найти описание цепочки (BchInfoEntry) по blockchainId.
|
||||
*
|
||||
*.
|
||||
* ─ Если описания нет (цепочка ещё не существует):
|
||||
* • принимаем только блок типа 0 (HeaderBody) и номера 0;
|
||||
* • парсим его, создаём новый BchInfoEntry на основе данных заголовка;
|
||||
@ -48,16 +48,16 @@ import java.util.Arrays;
|
||||
* • сохраняем блок и создаём новый blockchain-файл;
|
||||
* • добавляем цепочку в менеджер BchInfoManager.
|
||||
* (💡 временное решение: создание цепочки допустимо только через HeaderBody)
|
||||
*
|
||||
*.
|
||||
* ─ Если цепочка уже существует:
|
||||
* • проверяем, что номер блока равен (lastBlockNumber + 1);
|
||||
* • проверяем подпись и хэш;
|
||||
* • проверяем тело блока (check);
|
||||
* • добавляем блок в файл цепочки;
|
||||
* • обновляем состояние BchInfoEntry (номер, хэш, размер).
|
||||
*
|
||||
*.
|
||||
* 3️⃣ Если все проверки пройдены — возвращаем статус OK.
|
||||
*
|
||||
*.
|
||||
* Таким образом, единственное различие между первым блоком и последующими —
|
||||
* момент инициализации описания цепочки (BchInfoEntry).
|
||||
* Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково.
|
||||
|
||||
@ -6,6 +6,7 @@ import org.eclipse.jetty.websocket.api.annotations.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.InboundMessageProcessor;
|
||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.JsonInboundProcessor;
|
||||
|
||||
@ -24,6 +25,8 @@ public class BlockchainWsEndpoint {
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(Session session) {
|
||||
this.session = session;
|
||||
// Привязываем WebSocket-сессию к ConnectionContext
|
||||
connectionContext.setWsSession(session);
|
||||
log.info("WS connected: {}", session.getRemoteAddress());
|
||||
}
|
||||
|
||||
@ -77,6 +80,8 @@ public class BlockchainWsEndpoint {
|
||||
@OnWebSocketClose
|
||||
public void onClose(int statusCode, String reason) {
|
||||
log.info("WS closed: {} {}", statusCode, reason);
|
||||
// Удаляем это подключение из реестра активных соединений
|
||||
ActiveConnectionsRegistry.getInstance().remove(connectionContext);
|
||||
// На всякий случай очищаем контекст
|
||||
connectionContext.reset();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user