SHA256
11 12 25
Добавил Закрытие сессии
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ public class ConnectionContext {
|
||||
|
||||
// Статусы аутентификации
|
||||
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge, ждём CreateAuthSession
|
||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge
|
||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||
|
||||
// Полный пользователь из БД (solana_users)
|
||||
|
||||
+16
-12
@@ -1,15 +1,17 @@
|
||||
package server.logic.ws_protocol.JSON;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_AuthChallenge_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.*;
|
||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_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_CreateAuthSession__Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -25,18 +27,20 @@ import java.util.Map;
|
||||
public final class JsonHandlerRegistry {
|
||||
|
||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.of(
|
||||
"RefreshSession", new Net_RefreshSession_Handler(),
|
||||
"AddUser", new Net_AddUser_Handler(),
|
||||
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
||||
"CreateAuthSession", new Net_CreateAuthSession__Handler()
|
||||
"RefreshSession", new Net_RefreshSession_Handler(),
|
||||
"AddUser", new Net_AddUser_Handler(),
|
||||
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
||||
"CreateAuthSession", new Net_CreateAuthSession__Handler(),
|
||||
"CloseActiveSession", new Net_CloseActiveSession_Handler()
|
||||
// сюда потом добавишь другие операции
|
||||
);
|
||||
|
||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.of(
|
||||
"RefreshSession", Net_RefreshSession_Request.class,
|
||||
"AddUser", Net_AddUser_Request.class,
|
||||
"AuthChallenge", Net_AuthChallenge_Request.class,
|
||||
"CreateAuthSession", Net_CreateAuthSession_Request.class
|
||||
"RefreshSession", Net_RefreshSession_Request.class,
|
||||
"AddUser", Net_AddUser_Request.class,
|
||||
"AuthChallenge", Net_AuthChallenge_Request.class,
|
||||
"CreateAuthSession", Net_CreateAuthSession_Request.class,
|
||||
"CloseActiveSession", Net_CloseActiveSession_Request.class
|
||||
);
|
||||
|
||||
private JsonHandlerRegistry() {
|
||||
@@ -50,4 +54,4 @@ public final class JsonHandlerRegistry {
|
||||
public static Map<String, Class<? extends Net_Request>> getRequestTypes() {
|
||||
return REQUEST_TYPES;
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос CloseActiveSession — закрытие активной сессии пользователя.
|
||||
*
|
||||
* Допустимые режимы:
|
||||
*
|
||||
* 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-подключение.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ на CloseActiveSession.
|
||||
*
|
||||
* При успехе:
|
||||
* - status = 200;
|
||||
* - payload = {}.
|
||||
*
|
||||
* Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
|
||||
* или чуть позже (для текущей сессии) после отправки ответа.
|
||||
*/
|
||||
public class Net_CloseActiveSession_Response extends Net_Response {
|
||||
// Дополнительных полей пока не требуется.
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
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.Auth.Net_CloseActiveSession_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Response;
|
||||
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 server.ws.WsConnectionUtils;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.entities.ActiveSession;
|
||||
import shine.db.entities.SolanaUser;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Хэндлер CloseActiveSession.
|
||||
*
|
||||
* Назначение:
|
||||
* - закрыть одну из активных сессий пользователя:
|
||||
* * либо явно указанную в sessionId,
|
||||
* * либо текущую (если sessionId не задана).
|
||||
*
|
||||
* Допустимые состояния:
|
||||
* - AUTH_STATUS_USER:
|
||||
* * timeMs / signatureB64 могут быть пустыми.
|
||||
* * Достаточно факта текущей авторизации.
|
||||
*
|
||||
* - AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||
* * требуется проверка подписи Ed25519 над строкой
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||
* (authNonce взят на шаге AuthChallenge и хранится в ctx.authNonce).
|
||||
* * Если подпись корректна, можно закрывать сессию даже до полноценной
|
||||
* установки новой сессии.
|
||||
*
|
||||
* Закрытие:
|
||||
* - запись ActiveSession удаляется из БД;
|
||||
* - если по этой sessionId есть активное WebSocket-подключение:
|
||||
* * если это ДРУГОЕ подключение — оно закрывается сразу;
|
||||
* * если это ТЕКУЩЕЕ подключение — сначала отправляется ответ 200,
|
||||
* а закрытие выполняется в отдельном потоке с небольшой задержкой.
|
||||
*/
|
||||
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
|
||||
|
||||
@Override
|
||||
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) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"NOT_AUTHENTICATED",
|
||||
"Операция доступна только в состоянии авторизации или авторификации"
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUser user = ctx.getSolanaUser();
|
||||
long currentLoginId = user.getLoginId();
|
||||
|
||||
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) {
|
||||
targetSessionId = ctx.getSessionId();
|
||||
} else {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_SESSION_TO_CLOSE",
|
||||
"Не удалось определить, какую сессию нужно закрыть"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||
ActiveSession targetSession;
|
||||
try {
|
||||
targetSession = sessionsDao.getBySessionId(targetSessionId);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка доступа к базе данных при поиске сессии"
|
||||
);
|
||||
}
|
||||
|
||||
if (targetSession == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_NOT_FOUND",
|
||||
"Сессия для закрытия не найдена"
|
||||
);
|
||||
}
|
||||
|
||||
if (targetSession.getLoginId() != currentLoginId) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_OF_ANOTHER_USER",
|
||||
"Нельзя закрывать сессию другого пользователя"
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (SQLException e) {
|
||||
log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
|
||||
// Логируем, но считаем, что для клиента сессия всё равно должна быть недействительна.
|
||||
}
|
||||
|
||||
ConnectionContext ctxToClose =
|
||||
ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
|
||||
|
||||
if (ctxToClose == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCurrentSession && ctxToClose == currentCtx) {
|
||||
// Это текущее подключение: закрываем после отправки ответа.
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(50); // небольшая пауза, чтобы ответ ушёл
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
WsConnectionUtils.closeConnection(
|
||||
ctxToClose,
|
||||
4000,
|
||||
"Session closed by client via CloseActiveSession"
|
||||
);
|
||||
}, "CloseSession-" + targetSessionId).start();
|
||||
} else {
|
||||
// Другая сессия — можно закрыть сразу
|
||||
WsConnectionUtils.closeConnection(
|
||||
ctxToClose,
|
||||
4000,
|
||||
"Session closed by client via CloseActiveSession"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-13
@@ -54,7 +54,39 @@ 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();
|
||||
private static final long ALLOWED_SKEW_MS = 30_000L;
|
||||
|
||||
/** Допустимое расхождение времени клиента и сервера (мс). */
|
||||
public static final long ALLOWED_SKEW_MS = 30_000L;
|
||||
|
||||
/**
|
||||
* Общая проверка подписи Ed25519 над строкой:
|
||||
* "AUTHORIFICATED:" + timeMs + authNonce.
|
||||
*
|
||||
* Используется и в CreateAuthSession, и в CloseActiveSession (для статуса AUTH_IN_PROGRESS).
|
||||
*
|
||||
* @param user пользователь (используется deviceKey)
|
||||
* @param authNonce одноразовый nonce из шага 1
|
||||
* @param timeMs время на стороне клиента
|
||||
* @param signatureB64 подпись в base64
|
||||
* @return true — подпись корректна; false — подпись не проходит верификацию
|
||||
* @throws IllegalArgumentException при некорректном base64 ключа/подписи
|
||||
*/
|
||||
public static boolean verifyAuthorificatedSignature(
|
||||
SolanaUser 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 {
|
||||
@@ -147,11 +179,13 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
byte[] publicKey32;
|
||||
byte[] signature64;
|
||||
// --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 ---
|
||||
String authNonce = ctx.getAuthNonce();
|
||||
|
||||
// --- проверяем подпись через общий метод ---
|
||||
boolean sigOk;
|
||||
try {
|
||||
publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
|
||||
signature64 = Base64.getDecoder().decode(signatureB64);
|
||||
sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -163,14 +197,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
// --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 ---
|
||||
String authNonce = ctx.getAuthNonce();
|
||||
|
||||
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + authNonce ---
|
||||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||
if (!sigOk) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
|
||||
Reference in New Issue
Block a user