SHA256
23 01 25
Сделал авторификацию новую через sessionKey (Но пока тесты сессии падают)
This commit is contained in:
+66
-17
@@ -7,12 +7,22 @@ import shine.db.entities.ActiveSessionEntry;
|
||||
/**
|
||||
* ConnectionContext — контекст состояния одного WebSocket-соединения.
|
||||
* Живёт ровно столько же, сколько живёт подключение.
|
||||
*
|
||||
* Важно (v2):
|
||||
* - Авторизация всегда 2 шага:
|
||||
* A) Создание новой сессии через deviceKey:
|
||||
* AuthChallenge(login) -> ctx.authNonce
|
||||
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
|
||||
*
|
||||
* B) Вход в существующую сессию через sessionKey:
|
||||
* SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
|
||||
* SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
|
||||
*/
|
||||
public class ConnectionContext {
|
||||
|
||||
// Статусы аутентификации
|
||||
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge
|
||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||
|
||||
// Полный пользователь из БД (solana_users)
|
||||
@@ -23,20 +33,38 @@ public class ConnectionContext {
|
||||
|
||||
/**
|
||||
* Идентификатор сессии — base64-строка от 32 байт.
|
||||
* Заполняется после успешного входа (AUTH_STATUS_USER).
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* Секрет сессии (то, что хранится в active_sessions.session_pwd).
|
||||
*/
|
||||
private String sessionPwd;
|
||||
|
||||
/**
|
||||
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
||||
* используется на шаге 2 для проверки подписи.
|
||||
* используется на шаге CreateAuthSession для проверки подписи deviceKey.
|
||||
*/
|
||||
private String authNonce;
|
||||
|
||||
/* ===================== SessionLogin challenge (v2) ===================== */
|
||||
|
||||
/**
|
||||
* Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
|
||||
* используется на шаге SessionLogin для проверки подписи sessionKey.
|
||||
*/
|
||||
private String sessionLoginNonce;
|
||||
|
||||
/**
|
||||
* sessionId, для которого был выдан sessionLoginNonce.
|
||||
* Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
|
||||
*/
|
||||
private String sessionLoginSessionId;
|
||||
|
||||
/**
|
||||
* Время истечения sessionLoginNonce (мс с 1970-01-01).
|
||||
* Если текущее время > expiresAt, то nonce считается недействительным.
|
||||
*/
|
||||
private long sessionLoginNonceExpiresAtMs;
|
||||
|
||||
/* ====================================================================== */
|
||||
|
||||
/**
|
||||
* Текущий статус аутентификации.
|
||||
* См. константы AUTH_STATUS_*
|
||||
@@ -83,7 +111,7 @@ public class ConnectionContext {
|
||||
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
|
||||
}
|
||||
|
||||
// --- sessionId / sessionPwd ---
|
||||
// --- sessionId ---
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
@@ -93,14 +121,6 @@ public class ConnectionContext {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getSessionPwd() {
|
||||
return sessionPwd;
|
||||
}
|
||||
|
||||
public void setSessionPwd(String sessionPwd) {
|
||||
this.sessionPwd = sessionPwd;
|
||||
}
|
||||
|
||||
// --- authNonce ---
|
||||
|
||||
public String getAuthNonce() {
|
||||
@@ -111,6 +131,32 @@ public class ConnectionContext {
|
||||
this.authNonce = authNonce;
|
||||
}
|
||||
|
||||
// --- sessionLoginNonce (v2) ---
|
||||
|
||||
public String getSessionLoginNonce() {
|
||||
return sessionLoginNonce;
|
||||
}
|
||||
|
||||
public void setSessionLoginNonce(String sessionLoginNonce) {
|
||||
this.sessionLoginNonce = sessionLoginNonce;
|
||||
}
|
||||
|
||||
public String getSessionLoginSessionId() {
|
||||
return sessionLoginSessionId;
|
||||
}
|
||||
|
||||
public void setSessionLoginSessionId(String sessionLoginSessionId) {
|
||||
this.sessionLoginSessionId = sessionLoginSessionId;
|
||||
}
|
||||
|
||||
public long getSessionLoginNonceExpiresAtMs() {
|
||||
return sessionLoginNonceExpiresAtMs;
|
||||
}
|
||||
|
||||
public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
|
||||
this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
|
||||
}
|
||||
|
||||
// --- auth status ---
|
||||
|
||||
public int getAuthenticationStatus() {
|
||||
@@ -134,9 +180,12 @@ public class ConnectionContext {
|
||||
activeSessionEntry = null;
|
||||
|
||||
sessionId = null;
|
||||
sessionPwd = null;
|
||||
authNonce = null;
|
||||
|
||||
sessionLoginNonce = null;
|
||||
sessionLoginSessionId = null;
|
||||
sessionLoginNonceExpiresAtMs = 0;
|
||||
|
||||
authenticationStatus = AUTH_STATUS_NONE;
|
||||
wsSession = null;
|
||||
}
|
||||
|
||||
+30
-4
@@ -2,20 +2,32 @@ package server.logic.ws_protocol.JSON;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
||||
|
||||
// --- NEW v2 session login ---
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;
|
||||
|
||||
// --- auth entities ---
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_RefreshSession_Request;
|
||||
|
||||
// --- NEW v2 entities ---
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
|
||||
@@ -37,12 +49,19 @@ public final class JsonHandlerRegistry {
|
||||
|
||||
// Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
|
||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
||||
Map.entry("RefreshSession", new Net_RefreshSession_Handler()),
|
||||
Map.entry("AddUser", new Net_AddUser_Handler()),
|
||||
|
||||
// --- auth ---
|
||||
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
|
||||
Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
|
||||
Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
|
||||
Map.entry("ListSessions", new Net_ListSessions_Handler()),
|
||||
|
||||
// --- login to existing session in 2 steps ---
|
||||
Map.entry("SessionChallenge", new Net_SessionChallenge_Handler()),
|
||||
Map.entry("SessionLogin", new Net_SessionLogin_Handler()),
|
||||
|
||||
// --- blockchain ---
|
||||
Map.entry("AddBlock", new Net_AddBlock_Handler()),
|
||||
|
||||
// --- userParams ---
|
||||
@@ -55,12 +74,19 @@ public final class JsonHandlerRegistry {
|
||||
);
|
||||
|
||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||
Map.entry("RefreshSession", Net_RefreshSession_Request.class),
|
||||
Map.entry("AddUser", Net_AddUser_Request.class),
|
||||
|
||||
// --- auth ---
|
||||
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
|
||||
Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
|
||||
Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
|
||||
Map.entry("ListSessions", Net_ListSessions_Request.class),
|
||||
|
||||
// --- NEW v2 ---
|
||||
Map.entry("SessionChallenge", Net_SessionChallenge_Request.class),
|
||||
Map.entry("SessionLogin", Net_SessionLogin_Request.class),
|
||||
|
||||
// --- blockchain ---
|
||||
Map.entry("AddBlock", Net_AddBlock_Request.class),
|
||||
|
||||
// --- userParams ---
|
||||
|
||||
+15
-13
@@ -1,9 +1,11 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
@@ -13,10 +15,18 @@ import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Шаг 1 авторизации: запрос выдачи временного nonce (authNonce).
|
||||
* AuthChallenge (v2) — шаг 1 создания новой сессии.
|
||||
*
|
||||
* Клиент по логину просит сервер сгенерировать случайный authNonce,
|
||||
* который будет использован на втором шаге при подписи.
|
||||
* Логика авторизации (v2):
|
||||
* - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
|
||||
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
|
||||
* CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
|
||||
*
|
||||
* Что делает:
|
||||
* 1) Проверяет login.
|
||||
* 2) Находит пользователя (solana_users).
|
||||
* 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
|
||||
* 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
|
||||
*/
|
||||
public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
|
||||
@@ -47,9 +57,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// 2) Ищем пользователя в локальной БД
|
||||
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
|
||||
if (solanaUserEntry == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -59,21 +67,15 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// 3) Заполняем контекст пользователем
|
||||
ctx.setSolanaUser(solanaUserEntry);
|
||||
|
||||
// 3.1) Отмечаем, что по этому соединению начата авторификация
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
||||
|
||||
// 4) Генерируем одноразовый authNonce = base64(32 случайных байт)
|
||||
byte[] buf = new byte[32];
|
||||
RANDOM.nextBytes(buf);
|
||||
String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
|
||||
// Сохраняем challenge в отдельном поле authNonce
|
||||
ctx.setAuthNonce(authNonce);
|
||||
|
||||
// 5) Формируем ответ
|
||||
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
|
||||
+16
-128
@@ -4,10 +4,11 @@ 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.handlers.auth.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.ws.WsConnectionUtils;
|
||||
@@ -18,31 +19,15 @@ import shine.db.entities.SolanaUserEntry;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Хэндлер CloseActiveSession.
|
||||
* CloseActiveSession (v2) — закрытие текущей или другой сессии.
|
||||
*
|
||||
* Назначение:
|
||||
* - закрыть одну из активных сессий пользователя:
|
||||
* * либо явно указанную в sessionId,
|
||||
* * либо текущую (если sessionId не задана).
|
||||
*
|
||||
* Допустимые состояния:
|
||||
* - AUTH_STATUS_USER:
|
||||
* * timeMs / signatureB64 могут быть пустыми.
|
||||
* * Достаточно факта текущей авторизации.
|
||||
*
|
||||
* - AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||
* * требуется проверка подписи Ed25519 над строкой
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||
* (authNonce взят на шаге AuthChallenge и хранится в ctx.authNonce).
|
||||
* * Если подпись корректна, можно закрывать сессию даже до полноценной
|
||||
* установки новой сессии.
|
||||
* Логика авторизации (v2):
|
||||
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||
* - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
|
||||
*
|
||||
* Закрытие:
|
||||
* - запись ActiveSession удаляется из БД;
|
||||
* - если по этой sessionId есть активное WebSocket-подключение:
|
||||
* * если это ДРУГОЕ подключение — оно закрывается сразу;
|
||||
* * если это ТЕКУЩЕЕ подключение — сначала отправляется ответ 200,
|
||||
* а закрытие выполняется в отдельном потоке с небольшой задержкой.
|
||||
* - удаляем запись из БД
|
||||
* - если по sessionId есть активный WS — закрываем его
|
||||
*/
|
||||
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
|
||||
@@ -52,100 +37,24 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
||||
|
||||
if (ctx == null || ctx.getSolanaUser() == null) {
|
||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"NOT_AUTHENTICATED",
|
||||
"Операция доступна только в состоянии авторизации или авторификации"
|
||||
"Операция доступна только для авторизованных пользователей"
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUserEntry user = ctx.getSolanaUser();
|
||||
String currentLogin = user.getLogin();
|
||||
|
||||
int authStatus = ctx.getAuthenticationStatus();
|
||||
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
||||
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"BAD_AUTH_STATUS",
|
||||
"Операция CloseActiveSession недоступна в текущем статусе аутентификации"
|
||||
);
|
||||
}
|
||||
|
||||
// Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
||||
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||
String authNonce = ctx.getAuthNonce();
|
||||
if (authNonce == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_STEP1_CONTEXT",
|
||||
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
||||
);
|
||||
}
|
||||
|
||||
long timeMs = req.getTimeMs();
|
||||
String signatureB64 = req.getSignatureB64();
|
||||
|
||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SIGNATURE",
|
||||
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
||||
);
|
||||
}
|
||||
|
||||
long nowMs = System.currentTimeMillis();
|
||||
long diff = Math.abs(nowMs - timeMs);
|
||||
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"TIME_SKEW",
|
||||
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||
);
|
||||
}
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
||||
user,
|
||||
authNonce,
|
||||
timeMs,
|
||||
signatureB64
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"Некорректный формат Base64 для ключа или подписи"
|
||||
);
|
||||
}
|
||||
|
||||
if (!sigOk) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"BAD_SIGNATURE",
|
||||
"Подпись не прошла проверку"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем, какую sessionId закрывать
|
||||
String targetSessionId = req.getSessionId();
|
||||
if (targetSessionId == null || targetSessionId.isBlank()) {
|
||||
// Если sessionId не передана — берём текущую активную
|
||||
if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
|
||||
targetSessionId = ctx.getActiveSession().getSessionId();
|
||||
} else if (ctx.getSessionId() != null) {
|
||||
if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
|
||||
targetSessionId = ctx.getSessionId();
|
||||
} else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
|
||||
targetSessionId = ctx.getActiveSession().getSessionId();
|
||||
} else {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -156,10 +65,9 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||
ActiveSessionEntry targetSession;
|
||||
try {
|
||||
targetSession = sessionsDao.getBySessionId(targetSessionId);
|
||||
targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
@@ -190,50 +98,31 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
|
||||
boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
|
||||
|
||||
// Пытаемся удалить сессию из БД и закрыть соответствующее подключение
|
||||
closeActiveSession(targetSessionId, ctx, isCurrentSession);
|
||||
|
||||
// Ответ OK (payload станет {} в JsonInboundProcessor)
|
||||
Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
// Для текущей сессии WebSocket будет закрыт чуть позже в отдельном потоке,
|
||||
// чтобы этот ответ успел уйти.
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрытие активной сессии:
|
||||
* - удаление записи из БД;
|
||||
* - закрытие WebSocket-подключения, если оно существует.
|
||||
*
|
||||
* @param targetSessionId идентификатор сессии, которую надо закрыть
|
||||
* @param currentCtx контекст текущего подключения (которое вызвало запрос)
|
||||
* @param isCurrentSession true, если закрывается "эта же" сессия
|
||||
*/
|
||||
private void closeActiveSession(String targetSessionId,
|
||||
ConnectionContext currentCtx,
|
||||
boolean isCurrentSession) {
|
||||
|
||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||
try {
|
||||
sessionsDao.deleteBySessionId(targetSessionId);
|
||||
ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
|
||||
// Логируем, но считаем, что для клиента сессия всё равно должна быть недействительна.
|
||||
}
|
||||
|
||||
ConnectionContext ctxToClose =
|
||||
ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
|
||||
|
||||
if (ctxToClose == null) {
|
||||
return;
|
||||
}
|
||||
if (ctxToClose == null) return;
|
||||
|
||||
if (isCurrentSession && ctxToClose == currentCtx) {
|
||||
// Это текущее подключение: закрываем после отправки ответа.
|
||||
new Thread(() -> {
|
||||
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
|
||||
WsConnectionUtils.closeConnection(
|
||||
@@ -243,7 +132,6 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}, "CloseSession-" + targetSessionId).start();
|
||||
} else {
|
||||
// Другая сессия — можно закрыть сразу
|
||||
WsConnectionUtils.closeConnection(
|
||||
ctxToClose,
|
||||
4000,
|
||||
|
||||
+108
-50
@@ -1,14 +1,15 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
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.handlers.auth.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.ws.WsConnectionUtils;
|
||||
@@ -20,37 +21,37 @@ import shine.geo.GeoLookupService;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
||||
* - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
|
||||
* отправляет на сервер ТОЛЬКО sessionPubKeyB64.
|
||||
* - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
|
||||
*
|
||||
* Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
|
||||
*
|
||||
* На выходе:
|
||||
* - создаётся запись active_sessions
|
||||
* - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
|
||||
* - ответ: sessionId
|
||||
*/
|
||||
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
public static final long ALLOWED_SKEW_MS = 30_000L;
|
||||
|
||||
public static boolean verifyAuthorificatedSignature(
|
||||
SolanaUserEntry user,
|
||||
String authNonce,
|
||||
long timeMs,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
String pubKeyB64 = user.getDeviceKey();
|
||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
|
||||
byte[] signature64 = Base64.getDecoder().decode(signatureB64);
|
||||
|
||||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
|
||||
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
||||
|
||||
if (ctx == null
|
||||
@@ -93,6 +94,43 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
String sessionPubKeyB64 = req.getSessionPubKeyB64();
|
||||
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SESSION_PUBKEY",
|
||||
"Пустой sessionPubKeyB64"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
|
||||
return err;
|
||||
}
|
||||
|
||||
// Проверим, что ключ декодируется в 32 байта
|
||||
byte[] sessionPubKey32;
|
||||
try {
|
||||
sessionPubKey32 = decodeBase64Any(sessionPubKeyB64);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"Некорректный base64 в sessionPubKeyB64"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
|
||||
return err;
|
||||
}
|
||||
if (sessionPubKey32.length != 32) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_SESSION_PUBKEY_LEN",
|
||||
"sessionPubKey должен быть 32 байта"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
|
||||
return err;
|
||||
}
|
||||
|
||||
String signatureB64 = req.getSignatureB64();
|
||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
@@ -107,7 +145,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
|
||||
long timeMs = req.getTimeMs();
|
||||
long nowMs = System.currentTimeMillis();
|
||||
|
||||
long diff = Math.abs(nowMs - timeMs);
|
||||
if (diff > ALLOWED_SKEW_MS) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
@@ -125,15 +162,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||
}
|
||||
|
||||
String pubKeyB64 = user.getDeviceKey();
|
||||
if (pubKeyB64 == null || pubKeyB64.isBlank()) {
|
||||
String devicePubKeyB64 = user.getDeviceKey();
|
||||
if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_PUBKEY1",
|
||||
"Отсутствует публичный ключ pubkey1 для пользователя"
|
||||
"NO_DEVICE_KEY",
|
||||
"Отсутствует deviceKey у пользователя"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no pubkey");
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -141,7 +178,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64);
|
||||
sigOk = verifyCreateSessionSignature(
|
||||
user,
|
||||
login,
|
||||
authNonce,
|
||||
timeMs,
|
||||
sessionPubKeyB64,
|
||||
storagePwd,
|
||||
signatureB64
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -164,9 +209,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
// --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId ---
|
||||
String newSessionPwd = generateRandomSecret();
|
||||
String sessionId = generateRandomSessionId();
|
||||
// --- генерируем sessionId ---
|
||||
String sessionId = generateRandom32B64Url();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// --- Сбор данных о клиенте (IP, UA, язык) ---
|
||||
@@ -174,11 +218,12 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
|
||||
String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
|
||||
|
||||
String clientIp = null;
|
||||
String clientIp = "";
|
||||
if (wsSession != null) {
|
||||
clientIp = ClientInfoService.extractClientIp(wsSession);
|
||||
String ip = ClientInfoService.extractClientIp(wsSession);
|
||||
if (ip != null) clientIp = ip;
|
||||
|
||||
if (clientIp != null && !clientIp.isBlank()) {
|
||||
if (!clientIp.isBlank()) {
|
||||
try {
|
||||
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
||||
} catch (Exception e) {
|
||||
@@ -186,7 +231,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientIp == null) clientIp = "";
|
||||
|
||||
// --- создаём запись ActiveSession и сохраняем в БД ---
|
||||
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
|
||||
@@ -196,7 +240,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
activeSessionEntry = new ActiveSessionEntry(
|
||||
sessionId,
|
||||
login,
|
||||
newSessionPwd,
|
||||
sessionPubKeyB64, // session_key (pubkey)
|
||||
storagePwd,
|
||||
now,
|
||||
now,
|
||||
@@ -225,8 +269,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
// --- обновляем контекст ---
|
||||
ctx.setActiveSession(activeSessionEntry);
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии
|
||||
ctx.setAuthNonce(null); // одноразовый nonce больше не нужен
|
||||
ctx.setAuthNonce(null);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||
@@ -237,25 +280,40 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setSessionId(sessionId);
|
||||
resp.setSessionPwd(newSessionPwd);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация случайного sessionId: base64-строка от 32 байт.
|
||||
*/
|
||||
private String generateRandomSessionId() {
|
||||
private static boolean verifyCreateSessionSignature(
|
||||
SolanaUserEntry user,
|
||||
String login,
|
||||
String authNonce,
|
||||
long timeMs,
|
||||
String sessionPubKeyB64,
|
||||
String storagePwd,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
|
||||
byte[] signature64 = decodeBase64Any(signatureB64);
|
||||
|
||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce + ":" + sessionPubKeyB64 + ":" + storagePwd;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||
}
|
||||
|
||||
private static String generateRandom32B64Url() {
|
||||
byte[] buf = new byte[32];
|
||||
RANDOM.nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация случайного секрета (sessionPwd): base64-строка от 32 байт.
|
||||
*/
|
||||
private String generateRandomSecret() {
|
||||
byte[] buf = new byte[32];
|
||||
RANDOM.nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
|
||||
// сначала url-safe, потом обычный
|
||||
try {
|
||||
return Base64.getUrlDecoder().decode(s);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return Base64.getDecoder().decode(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-91
@@ -3,12 +3,13 @@ package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
@@ -20,16 +21,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Хэндлер ListSessions.
|
||||
* ListSessions (v2) — список активных сессий.
|
||||
*
|
||||
* Назначение:
|
||||
* - вернуть список всех активных сессий текущего пользователя
|
||||
* (по loginId из ctx/solanaUser).
|
||||
*
|
||||
* Безопасность:
|
||||
* - анонимный клиент → NOT_AUTHENTICATED (UNVERIFIED);
|
||||
* - AUTH_STATUS_USER → достаточно факта авторизации;
|
||||
* - AUTH_STATUS_AUTH_IN_PROGRESS → требуется подпись, как в CreateAuthSession/CloseActiveSession.
|
||||
* Логика авторизации (v2):
|
||||
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||
* - Никаких подписей здесь больше нет.
|
||||
*/
|
||||
public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
|
||||
@@ -39,8 +35,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
||||
|
||||
// 1) Проверяем, что вообще есть пользователь в контексте
|
||||
if (ctx == null || ctx.getSolanaUser() == null) {
|
||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
@@ -52,81 +47,6 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
SolanaUserEntry user = ctx.getSolanaUser();
|
||||
String currentLogin = user.getLogin();
|
||||
|
||||
int authStatus = ctx.getAuthenticationStatus();
|
||||
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
||||
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"BAD_AUTH_STATUS",
|
||||
"Операция ListSessions недоступна в текущем статусе аутентификации"
|
||||
);
|
||||
}
|
||||
|
||||
// 2) Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
||||
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||
String authNonce = ctx.getAuthNonce();
|
||||
if (authNonce == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_STEP1_CONTEXT",
|
||||
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
||||
);
|
||||
}
|
||||
|
||||
long timeMs = req.getTimeMs();
|
||||
String signatureB64 = req.getSignatureB64();
|
||||
|
||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SIGNATURE",
|
||||
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
||||
);
|
||||
}
|
||||
|
||||
long nowMs = System.currentTimeMillis();
|
||||
long diff = Math.abs(nowMs - timeMs);
|
||||
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"TIME_SKEW",
|
||||
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||
);
|
||||
}
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
||||
user,
|
||||
authNonce,
|
||||
timeMs,
|
||||
signatureB64
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"Некорректный формат Base64 для ключа или подписи"
|
||||
);
|
||||
}
|
||||
|
||||
if (!sigOk) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"BAD_SIGNATURE",
|
||||
"Подпись не прошла проверку"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Тянем все активные сессии пользователя из БД
|
||||
List<ActiveSessionEntry> sessions;
|
||||
try {
|
||||
sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
|
||||
@@ -140,10 +60,9 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Собираем DTO с геолокацией
|
||||
List<SessionInfo> resultList = new ArrayList<>();
|
||||
for (ActiveSessionEntry s : sessions) {
|
||||
SessionInfo info = new Net_ListSessions_Response.SessionInfo();
|
||||
SessionInfo info = new SessionInfo();
|
||||
info.setSessionId(s.getSessionId());
|
||||
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
||||
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
||||
@@ -156,7 +75,6 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
resultList.add(info);
|
||||
}
|
||||
|
||||
// 5) Формируем ответ
|
||||
Net_ListSessions_Response resp = new Net_ListSessions_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
|
||||
+30
-200
@@ -1,200 +1,30 @@
|
||||
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.handlers.auth.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.geo.ClientInfoService;
|
||||
import shine.geo.GeoLookupService;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Хэндлер RefreshSession.
|
||||
*
|
||||
* При успешной проверке sessionId + sessionPwd:
|
||||
* - подтягивает пользователя по loginId из сессии;
|
||||
* - заполняет ConnectionContext;
|
||||
* - обновляет lastAuthirificatedAtMs и метаданные сессии в БД;
|
||||
* - возвращает storagePwd в payload.
|
||||
*/
|
||||
public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_RefreshSession_Handler.class);
|
||||
|
||||
// максимум 50 символов для clientInfo от клиента
|
||||
private static final int CLIENT_INFO_MAX_LEN = 50;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception {
|
||||
Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
||||
|
||||
String sessionId = req.getSessionId();
|
||||
String sessionPwd = req.getSessionPwd();
|
||||
String clientInfoFromClient = trimClientInfo(req.getClientInfo());
|
||||
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_SESSION_ID",
|
||||
"Пустой идентификатор сессии"
|
||||
);
|
||||
}
|
||||
|
||||
if (sessionPwd == null || sessionPwd.isEmpty()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_SESSION_PWD",
|
||||
"Пустой пароль сессии"
|
||||
);
|
||||
}
|
||||
|
||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||
ActiveSessionEntry session;
|
||||
try {
|
||||
session = sessionsDao.getBySessionId(sessionId);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при поиске сессии sessionId={}", sessionId, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка доступа к базе данных"
|
||||
);
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_NOT_FOUND",
|
||||
"Сессия не найдена"
|
||||
);
|
||||
}
|
||||
|
||||
String dbPwd = session.getSessionPwd();
|
||||
if (dbPwd == null || !dbPwd.equals(sessionPwd)) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_PWD_MISMATCH",
|
||||
"Неверный пароль сессии"
|
||||
);
|
||||
}
|
||||
|
||||
// --- вытаскиваем пользователя по login из сессии ---
|
||||
SolanaUserEntry solanaUserEntry;
|
||||
String login = session.getLogin();
|
||||
|
||||
try {
|
||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
solanaUserEntry = usersDao.getByLogin(login);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при поиске пользователя по login={} из сессии", login, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR_USER_LOOKUP",
|
||||
"Ошибка доступа к базе данных при получении пользователя для сессии"
|
||||
);
|
||||
}
|
||||
|
||||
if (solanaUserEntry == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"USER_NOT_FOUND_FOR_SESSION",
|
||||
"Пользователь для данной сессии не найден"
|
||||
);
|
||||
}
|
||||
|
||||
// --- собираем данные о клиенте из WebSocket-запроса ---
|
||||
String clientIp = null;
|
||||
String clientInfoFromRequest = null;
|
||||
String userLanguage = null;
|
||||
|
||||
if (ctx != null && ctx.getWsSession() != null) {
|
||||
// Нормальное получение IP-адреса клиента
|
||||
clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
|
||||
|
||||
// Сделать запрос геолокации и никуда её не сохранять:
|
||||
// вызов с кэшированием в БД, нужно только для прогрева кэша.
|
||||
if (clientIp != null && !clientIp.isBlank()) {
|
||||
try {
|
||||
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
||||
} catch (Exception e) {
|
||||
// Геолокация не критична, просто логируем на debug/trace при желании
|
||||
log.debug("Geo lookup failed for ip={}", clientIp, e);
|
||||
}
|
||||
}
|
||||
|
||||
clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
|
||||
userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
|
||||
}
|
||||
|
||||
long nowMs = System.currentTimeMillis();
|
||||
|
||||
// --- обновляем запись в БД (lastAuth + мета) ---
|
||||
try {
|
||||
sessionsDao.updateOnRefresh(
|
||||
sessionId,
|
||||
nowMs,
|
||||
clientIp,
|
||||
clientInfoFromClient,
|
||||
clientInfoFromRequest,
|
||||
userLanguage
|
||||
);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при обновлении метаданных сессии sessionId={}", sessionId, e);
|
||||
// не роняем авторизацию, но логируем
|
||||
}
|
||||
|
||||
// Также обновим объект session в памяти (если дальше кто-то его использует)
|
||||
session.setLastAuthirificatedAtMs(nowMs);
|
||||
session.setClientIp(clientIp);
|
||||
session.setClientInfoFromClient(clientInfoFromClient);
|
||||
session.setClientInfoFromRequest(clientInfoFromRequest);
|
||||
session.setUserLanguage(userLanguage);
|
||||
|
||||
// --- обновляем контекст соединения ---
|
||||
if (ctx != null) {
|
||||
ctx.setActiveSession(session);
|
||||
ctx.setSolanaUser(solanaUserEntry);
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setSessionPwd(sessionPwd);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
// Регистрируем это подключение в глобальном реестре активных соединений
|
||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||
}
|
||||
|
||||
// --- ответ OK + storagePwd ---
|
||||
Net_RefreshSession_Response resp = new Net_RefreshSession_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setStoragePwd(session.getStoragePwd());
|
||||
return resp;
|
||||
}
|
||||
|
||||
private String trimClientInfo(String info) {
|
||||
if (info == null) return null;
|
||||
info = info.trim();
|
||||
if (info.length() > CLIENT_INFO_MAX_LEN) {
|
||||
return info.substring(0, CLIENT_INFO_MAX_LEN);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
//package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
//
|
||||
//import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
//import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
//import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_RefreshSession_Request;
|
||||
//import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
//import server.logic.ws_protocol.WireCodes;
|
||||
//
|
||||
///**
|
||||
// * RefreshSession (v2) — ОТКЛЮЧЕН.
|
||||
// *
|
||||
// * Раньше это был "короткий вход" (1 запрос) по sessionId+sessionPwd.
|
||||
// * Теперь вход всегда 2 шага: SessionChallenge -> SessionLogin (подпись sessionKey).
|
||||
// */
|
||||
//public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
||||
//
|
||||
// @Override
|
||||
// public Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception {
|
||||
// Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
||||
//
|
||||
// return NetExceptionResponseFactory.error(
|
||||
// req,
|
||||
// WireCodes.Status.GONE, // 410
|
||||
// "DISABLED_V2",
|
||||
// "RefreshSession отключён в v2. Используй SessionChallenge + SessionLogin."
|
||||
// );
|
||||
// }
|
||||
//}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* SessionChallenge (v2) — шаг 1 входа в существующую сессию.
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - Вход в существующую сессию ВСЕГДА в 2 шага:
|
||||
* 1) SessionChallenge(sessionId) -> nonce
|
||||
* 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
|
||||
*
|
||||
* Что делает:
|
||||
* - Проверяет, что sessionId существует в БД.
|
||||
* - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
|
||||
* ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
|
||||
*/
|
||||
public class Net_SessionChallenge_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final long NONCE_TTL_MS = 60_000L;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
|
||||
|
||||
String sessionId = req.getSessionId();
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SESSION_ID",
|
||||
"Пустой sessionId"
|
||||
);
|
||||
}
|
||||
|
||||
ActiveSessionEntry session;
|
||||
try {
|
||||
session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||
} catch (SQLException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка доступа к базе данных"
|
||||
);
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_NOT_FOUND",
|
||||
"Сессия не найдена"
|
||||
);
|
||||
}
|
||||
|
||||
byte[] buf = new byte[32];
|
||||
RANDOM.nextBytes(buf);
|
||||
String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
ctx.setSessionLoginNonce(nonce);
|
||||
ctx.setSessionLoginSessionId(sessionId);
|
||||
ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
|
||||
|
||||
Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setNonce(nonce);
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
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.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.geo.ClientInfoService;
|
||||
import shine.geo.GeoLookupService;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
|
||||
* - SessionLogin проверяет подпись sessionKey над строкой:
|
||||
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||
* - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
|
||||
*
|
||||
* При успехе:
|
||||
* - ctx становится AUTH_STATUS_USER
|
||||
* - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
|
||||
* - возвращаем storagePwd
|
||||
*/
|
||||
public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
|
||||
|
||||
private static final long ALLOWED_SKEW_MS = 30_000L;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
|
||||
|
||||
String sessionId = req.getSessionId();
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SESSION_ID",
|
||||
"Пустой sessionId"
|
||||
);
|
||||
}
|
||||
|
||||
// проверка челленджа
|
||||
if (ctx.getSessionLoginNonce() == null
|
||||
|| ctx.getSessionLoginSessionId() == null
|
||||
|| System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_CHALLENGE",
|
||||
"Нет активного SessionChallenge или nonce истёк"
|
||||
);
|
||||
}
|
||||
|
||||
if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"SESSION_ID_MISMATCH",
|
||||
"nonce был выдан для другого sessionId"
|
||||
);
|
||||
}
|
||||
|
||||
long timeMs = req.getTimeMs();
|
||||
long nowMs = System.currentTimeMillis();
|
||||
if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"TIME_SKEW",
|
||||
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||
);
|
||||
}
|
||||
|
||||
String signatureB64 = req.getSignatureB64();
|
||||
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SIGNATURE",
|
||||
"Пустая подпись"
|
||||
);
|
||||
}
|
||||
|
||||
ActiveSessionEntry session;
|
||||
try {
|
||||
session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||
} catch (SQLException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка доступа к базе данных"
|
||||
);
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_NOT_FOUND",
|
||||
"Сессия не найдена"
|
||||
);
|
||||
}
|
||||
|
||||
String sessionPubKeyB64 = session.getSessionKey(); // это pubKey
|
||||
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"NO_SESSION_KEY",
|
||||
"В сессии не задан session_key"
|
||||
);
|
||||
}
|
||||
|
||||
String nonce = ctx.getSessionLoginNonce();
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"Некорректный Base64 для ключа/подписи"
|
||||
);
|
||||
}
|
||||
|
||||
if (!sigOk) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"BAD_SIGNATURE",
|
||||
"Подпись не прошла проверку"
|
||||
);
|
||||
}
|
||||
|
||||
// сжигаем nonce
|
||||
ctx.setSessionLoginNonce(null);
|
||||
ctx.setSessionLoginSessionId(null);
|
||||
ctx.setSessionLoginNonceExpiresAtMs(0);
|
||||
|
||||
// подтягиваем пользователя
|
||||
SolanaUserEntry user;
|
||||
try {
|
||||
user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
|
||||
} catch (SQLException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR_USER_LOOKUP",
|
||||
"Ошибка доступа к базе данных при получении пользователя"
|
||||
);
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"USER_NOT_FOUND_FOR_SESSION",
|
||||
"Пользователь для данной сессии не найден"
|
||||
);
|
||||
}
|
||||
|
||||
// обновление метаданных
|
||||
String clientInfoFromClient = req.getClientInfo();
|
||||
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
|
||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||
}
|
||||
|
||||
String clientIp = null;
|
||||
String clientInfoFromRequest = null;
|
||||
String userLanguage = null;
|
||||
|
||||
if (ctx.getWsSession() != null) {
|
||||
clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
|
||||
clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
|
||||
userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
|
||||
|
||||
if (clientIp != null && !clientIp.isBlank()) {
|
||||
try {
|
||||
GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
|
||||
} catch (Exception e) {
|
||||
log.debug("Geo lookup failed for ip={}", clientIp, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
try {
|
||||
ActiveSessionsDAO.getInstance().updateOnRefresh(
|
||||
sessionId,
|
||||
now,
|
||||
clientIp,
|
||||
clientInfoFromClient,
|
||||
clientInfoFromRequest,
|
||||
userLanguage
|
||||
);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
|
||||
}
|
||||
|
||||
session.setLastAuthirificatedAtMs(now);
|
||||
session.setClientIp(clientIp);
|
||||
session.setClientInfoFromClient(clientInfoFromClient);
|
||||
session.setClientInfoFromRequest(clientInfoFromRequest);
|
||||
session.setUserLanguage(userLanguage);
|
||||
|
||||
// ctx
|
||||
ctx.setActiveSession(session);
|
||||
ctx.setSolanaUser(user);
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||
|
||||
// ответ
|
||||
Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setStoragePwd(session.getStoragePwd());
|
||||
return resp;
|
||||
}
|
||||
|
||||
private static boolean verifySessionLoginSignature(
|
||||
String sessionPubKeyB64,
|
||||
String sessionId,
|
||||
long timeMs,
|
||||
String nonce,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
|
||||
byte[] signature64 = decodeBase64Any(signatureB64);
|
||||
|
||||
String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||
}
|
||||
|
||||
private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
|
||||
try {
|
||||
return Base64.getUrlDecoder().decode(s);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return Base64.getDecoder().decode(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-38
@@ -5,35 +5,20 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
/**
|
||||
* Запрос CloseActiveSession — закрытие активной сессии пользователя.
|
||||
*
|
||||
* Допустимые режимы:
|
||||
* Новая логика (v2):
|
||||
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||
* - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
|
||||
*
|
||||
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
||||
* - поле sessionId:
|
||||
* * если заполнено — закрывается указанная сессия пользователя;
|
||||
* * если пустое — закрывается текущая авторизованная сессия
|
||||
* (та, в рамках которой выполняется запрос).
|
||||
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
||||
*
|
||||
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||
* - требуется дополнительно подтвердить владение ключом:
|
||||
* * timeMs — время на клиенте (мс с 1970-01-01),
|
||||
* * signatureB64 — подпись Ed25519 над строкой
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce.
|
||||
* - authNonce берётся из шага 1 (AuthChallenge) и хранится в ctx.authNonce.
|
||||
* - если подпись корректна, сервер закрывает указанную sessionId (или текущую,
|
||||
* если sessionId не задана) и рвёт соответствующее WebSocket-подключение.
|
||||
* payload:
|
||||
* {
|
||||
* "sessionId": "..." // опционально; если пусто — закрываем текущую
|
||||
* }
|
||||
*/
|
||||
public class Net_CloseActiveSession_Request extends Net_Request {
|
||||
|
||||
/** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
|
||||
private String sessionId;
|
||||
|
||||
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
||||
private long timeMs;
|
||||
|
||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||
private String signatureB64;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
@@ -41,20 +26,4 @@ public class Net_CloseActiveSession_Request extends Net_Request {
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
public void setTimeMs(long timeMs) {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public String getSignatureB64() {
|
||||
return signatureB64;
|
||||
}
|
||||
|
||||
public void setSignatureB64(String signatureB64) {
|
||||
this.signatureB64 = signatureB64;
|
||||
}
|
||||
}
|
||||
+22
-23
@@ -3,40 +3,31 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
|
||||
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
|
||||
*
|
||||
* Клиент:
|
||||
* 1) получает от сервера authNonce на шаге 1;
|
||||
* 2) генерирует свой StoragePwd (base64 от 32 байт);
|
||||
* 3) формирует строку для подписи:
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||
* 4) подписывает эту строку своим приватным ключом (pubkey1),
|
||||
* отправляет подпись и StoragePwd на сервер.
|
||||
* Шаги:
|
||||
* 1) AuthChallenge(login) -> authNonce
|
||||
* 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
|
||||
*
|
||||
* Дополнительно:
|
||||
* - clientInfo — короткая строка (до 50 символов) с данными об устройстве/клиенте.
|
||||
* Подпись deviceKey делается над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
|
||||
*
|
||||
* Формат входящего JSON:
|
||||
* {
|
||||
* "op": "CreateAuthSession",
|
||||
* "requestId": "...",
|
||||
* "payload": {
|
||||
* "storagePwd": "base64-строка-от-32-байт",
|
||||
* "timeMs": 1733310000000,
|
||||
* "signatureB64": "base64-подпись-Ed25519",
|
||||
* "clientInfo": "Chrome/Android" // опционально, до 50 символов
|
||||
* }
|
||||
* }
|
||||
* Важно:
|
||||
* - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
|
||||
* - В БД active_sessions.session_key хранится sessionPubKeyB64.
|
||||
*/
|
||||
public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
|
||||
/** Клиентский пароль для хранения данных (base64 от 32 байт). */
|
||||
/** Клиентский пароль для хранения данных (base64url от 32 байт). */
|
||||
private String storagePwd;
|
||||
|
||||
/** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
|
||||
private String sessionPubKeyB64;
|
||||
|
||||
/** Время на стороне клиента (мс с 1970-01-01). */
|
||||
private long timeMs;
|
||||
|
||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||
/** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||
private String signatureB64;
|
||||
|
||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||
@@ -50,6 +41,14 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
this.storagePwd = storagePwd;
|
||||
}
|
||||
|
||||
public String getSessionPubKeyB64() {
|
||||
return sessionPubKeyB64;
|
||||
}
|
||||
|
||||
public void setSessionPubKeyB64(String sessionPubKeyB64) {
|
||||
this.sessionPubKeyB64 = sessionPubKeyB64;
|
||||
}
|
||||
|
||||
public long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
+4
-16
@@ -3,10 +3,10 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ на CreateAuthSession.
|
||||
* Ответ на CreateAuthSession (v2).
|
||||
*
|
||||
* При успехе сервер создаёт запись в active_sessions
|
||||
* и возвращает идентификатор сессии sessionId и секрет сессии sessionPwd.
|
||||
* и возвращает идентификатор сессии sessionId.
|
||||
*
|
||||
* JSON:
|
||||
* {
|
||||
@@ -14,19 +14,15 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
* "requestId": "...",
|
||||
* "status": 200,
|
||||
* "payload": {
|
||||
* "sessionId": "base64-строка-от-32-байт",
|
||||
* "sessionPwd": "base64-строка-от-32-байт"
|
||||
* "sessionId": "base64url(32)"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public class Net_CreateAuthSession_Response extends Net_Response {
|
||||
|
||||
/** Идентификатор сессии, base64 от 32 байт. */
|
||||
/** Идентификатор сессии, base64url от 32 байт. */
|
||||
private String sessionId;
|
||||
|
||||
/** Секрет сессии, base64 от 32 байт. */
|
||||
private String sessionPwd;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
@@ -34,12 +30,4 @@ public class Net_CreateAuthSession_Response extends Net_Response {
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getSessionPwd() {
|
||||
return sessionPwd;
|
||||
}
|
||||
|
||||
public void setSessionPwd(String sessionPwd) {
|
||||
this.sessionPwd = sessionPwd;
|
||||
}
|
||||
}
|
||||
+4
-44
@@ -5,50 +5,10 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
/**
|
||||
* Запрос ListSessions — список активных сессий пользователя.
|
||||
*
|
||||
* Режимы безопасности такие же, как у CloseActiveSession:
|
||||
*
|
||||
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
||||
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
||||
*
|
||||
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||
* - требуется подпись Ed25519 над строкой
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||
* (authNonce сохранён в ctx.authNonce после AuthChallenge).
|
||||
*
|
||||
* 3) Анонимный клиент (AUTH_STATUS_NONE или нет пользователя в ctx):
|
||||
* - возвращается ошибка NOT_AUTHENTICATED.
|
||||
*
|
||||
* JSON:
|
||||
* {
|
||||
* "op": "ListSessions",
|
||||
* "requestId": "...",
|
||||
* "payload": {
|
||||
* "timeMs": 1733310000000, // при AUTH_IN_PROGRESS
|
||||
* "signatureB64": "base64-подпись" // при AUTH_IN_PROGRESS
|
||||
* }
|
||||
* }
|
||||
* Новая логика (v2):
|
||||
* - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
|
||||
* - Пустой payload.
|
||||
*/
|
||||
public class Net_ListSessions_Request extends Net_Request {
|
||||
|
||||
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
||||
private long timeMs;
|
||||
|
||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||
private String signatureB64;
|
||||
|
||||
public long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
public void setTimeMs(long timeMs) {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public String getSignatureB64() {
|
||||
return signatureB64;
|
||||
}
|
||||
|
||||
public void setSignatureB64(String signatureB64) {
|
||||
this.signatureB64 = signatureB64;
|
||||
}
|
||||
// пусто
|
||||
}
|
||||
+36
-48
@@ -1,48 +1,36 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос RefreshSession.
|
||||
*
|
||||
* Используется для повторного входа без повторной подписи:
|
||||
* клиент хранит sessionId и sessionPwd, которые получил на шаге 2.
|
||||
*
|
||||
* JSON (payload):
|
||||
* {
|
||||
* "sessionId": "base64-id-сессии",
|
||||
* "sessionPwd": "base64-sessionPwd",
|
||||
* "clientInfo": "до 50 символов, краткая строка об устройстве"
|
||||
* }
|
||||
*/
|
||||
public class Net_RefreshSession_Request extends Net_Request {
|
||||
|
||||
private String sessionId;
|
||||
private String sessionPwd;
|
||||
|
||||
/**
|
||||
* Краткая строка с информацией об устройстве/клиенте, до 50 символов.
|
||||
* Например: "PWA/Chrome/Android".
|
||||
*/
|
||||
private String clientInfo;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getSessionPwd() {
|
||||
return sessionPwd;
|
||||
}
|
||||
|
||||
public void setSessionPwd(String sessionPwd) {
|
||||
this.sessionPwd = sessionPwd;
|
||||
}
|
||||
|
||||
public String getClientInfo() { return clientInfo; }
|
||||
|
||||
public void setClientInfo(String clientInfo) { this.clientInfo = clientInfo; }
|
||||
}
|
||||
//package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
//
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
//
|
||||
///**
|
||||
// * Запрос RefreshSession.
|
||||
// *
|
||||
// * В новой версии (v2) RefreshSession ОТКЛЮЧЕН.
|
||||
// * Оставлен временно для совместимости, handler вернёт 410 GONE.
|
||||
// */
|
||||
//public class Net_RefreshSession_Request extends Net_Request {
|
||||
//
|
||||
// private String sessionId;
|
||||
// private String sessionPwd;
|
||||
// private String clientInfo;
|
||||
//
|
||||
// public String getSessionId() {
|
||||
// return sessionId;
|
||||
// }
|
||||
//
|
||||
// public void setSessionId(String sessionId) {
|
||||
// this.sessionId = sessionId;
|
||||
// }
|
||||
//
|
||||
// public String getSessionPwd() {
|
||||
// return sessionPwd;
|
||||
// }
|
||||
//
|
||||
// public void setSessionPwd(String sessionPwd) {
|
||||
// this.sessionPwd = sessionPwd;
|
||||
// }
|
||||
//
|
||||
// public String getClientInfo() { return clientInfo; }
|
||||
//
|
||||
// public void setClientInfo(String clientInfo) { this.clientInfo = clientInfo; }
|
||||
//}
|
||||
+23
-33
@@ -1,33 +1,23 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Успешный ответ на RefreshSession.
|
||||
*
|
||||
* Дополнительно к статусу 200 сервер возвращает storagePwd,
|
||||
* чтобы клиент мог восстановить/синхронизировать локальное хранилище.
|
||||
*
|
||||
* JSON:
|
||||
* {
|
||||
* "op": "RefreshSession",
|
||||
* "requestId": "...",
|
||||
* "status": 200,
|
||||
* "payload": {
|
||||
* "storagePwd": "base64-строка-от-32-байт"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public class Net_RefreshSession_Response extends Net_Response {
|
||||
|
||||
/** Пароль хранилища, сохранённый в сессии (storagePwd). */
|
||||
private String storagePwd;
|
||||
|
||||
public String getStoragePwd() {
|
||||
return storagePwd;
|
||||
}
|
||||
|
||||
public void setStoragePwd(String storagePwd) {
|
||||
this.storagePwd = storagePwd;
|
||||
}
|
||||
}
|
||||
//package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
//
|
||||
//import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
//
|
||||
///**
|
||||
// * Ответ на RefreshSession.
|
||||
// *
|
||||
// * В новой версии (v2) RefreshSession ОТКЛЮЧЕН.
|
||||
// * Этот класс можно оставить временно для совместимости сериализации,
|
||||
// * но handler будет возвращать 410 GONE.
|
||||
// */
|
||||
//public class Net_RefreshSession_Response extends Net_Response {
|
||||
//
|
||||
// private String storagePwd;
|
||||
//
|
||||
// public String getStoragePwd() {
|
||||
// return storagePwd;
|
||||
// }
|
||||
//
|
||||
// public void setStoragePwd(String storagePwd) {
|
||||
// this.storagePwd = storagePwd;
|
||||
// }
|
||||
//}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Шаг 1 входа в существующую сессию (v2):
|
||||
* SessionChallenge(sessionId) -> nonce
|
||||
*/
|
||||
public class Net_SessionChallenge_Request extends Net_Request {
|
||||
|
||||
private String sessionId;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ на SessionChallenge (v2).
|
||||
* payload: { "nonce": "base64url(32)" }
|
||||
*/
|
||||
public class Net_SessionChallenge_Response extends Net_Response {
|
||||
|
||||
private String nonce;
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public void setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Шаг 2 входа в существующую сессию (v2):
|
||||
* SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
|
||||
*
|
||||
* Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
|
||||
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||
*
|
||||
* nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
|
||||
*/
|
||||
public class Net_SessionLogin_Request extends Net_Request {
|
||||
|
||||
private String sessionId;
|
||||
private long timeMs;
|
||||
private String signatureB64;
|
||||
|
||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||
private String clientInfo;
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
public void setTimeMs(long timeMs) {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public String getSignatureB64() {
|
||||
return signatureB64;
|
||||
}
|
||||
|
||||
public void setSignatureB64(String signatureB64) {
|
||||
this.signatureB64 = signatureB64;
|
||||
}
|
||||
|
||||
public String getClientInfo() {
|
||||
return clientInfo;
|
||||
}
|
||||
|
||||
public void setClientInfo(String clientInfo) {
|
||||
this.clientInfo = clientInfo;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ на SessionLogin (v2).
|
||||
* payload: { "storagePwd": "base64url(32)" }
|
||||
*/
|
||||
public class Net_SessionLogin_Response extends Net_Response {
|
||||
|
||||
private String storagePwd;
|
||||
|
||||
public String getStoragePwd() {
|
||||
return storagePwd;
|
||||
}
|
||||
|
||||
public void setStoragePwd(String storagePwd) {
|
||||
this.storagePwd = storagePwd;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user