промежуточный не рабочий комит
This commit is contained in:
AidarKC
2025-12-10 16:15:36 +03:00
parent 95ec6ba037
commit 00fc9e3926
16 changed files with 506 additions and 146 deletions
+2
View File
@@ -29,6 +29,8 @@ dependencies {
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
implementation project(':shine-server-geo') // модуль для определения геолокации по IP
}
@@ -5,8 +5,9 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/**
* Ответ на AuthChallenge.
*
* При успехе сервер возвращает временный секрет sessionPwd,
* который клиент обязан использовать на втором шаге при формировании подписи.
* При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
* который клиент обязан использовать на втором шаге при формировании строки
* для цифровой подписи.
*
* JSON:
* {
@@ -14,22 +15,23 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
* "requestId": "...",
* "status": 200,
* "payload": {
* "sessionPwd": "base64-строка-от-32-байт"
* "authNonce": "base64-строка-от-32-байт"
* }
* }
*/
public class Net_AuthChallenge_Response extends NetResponse {
/**
* Временный секрет, сгенерированный сервером.
* Одноразовый nonce для авторификации.
* Строка — это base64-представление 32 случайных байт.
*/
private String sessionPwd;
private String authNonce;
public String getSessionPwd() {
return sessionPwd;
public String getAuthNonce() {
return authNonce;
}
public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd;
public void setAuthNonce(String authNonce) {
this.authNonce = authNonce;
}
}
}
@@ -6,13 +6,16 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
* Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
*
* Клиент:
* 1) получает от сервера sessionPwd на шаге 1;
* 1) получает от сервера authNonce на шаге 1;
* 2) генерирует свой StoragePwd (base64 от 32 байт);
* 3) формирует строку для подписи:
* "AUTHORIFICATED:" + timeMs + sessionPwd
* "AUTHORIFICATED:" + timeMs + authNonce
* 4) подписывает эту строку своим приватным ключом (pubkey1),
* отправляет подпись и StoragePwd на сервер.
*
* Дополнительно:
* - clientInfo — короткая строка (до 50 символов) с данными об устройстве/клиенте.
*
* Формат входящего JSON:
* {
* "op": "CreateAuthSession",
@@ -20,12 +23,10 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
* "payload": {
* "storagePwd": "base64-строка-от-32-байт",
* "timeMs": 1733310000000,
* "signatureB64": "base64-подпись-Ed25519"
* "signatureB64": "base64-подпись-Ed25519",
* "clientInfo": "Chrome/Android" // опционально, до 50 символов
* }
* }
*
* При успешной проверке подписи сервер создаёт запись в active_sessions
* и возвращает sessionId (base64-строка от 32 байт).
*/
public class Net_CreateAuthSession_Request extends NetRequest {
@@ -35,9 +36,12 @@ public class Net_CreateAuthSession_Request extends NetRequest {
/** Время на стороне клиента (мс с 1970-01-01). */
private long timeMs;
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + sessionPwd (base64). */
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
private String signatureB64;
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
private String clientInfo;
public String getStoragePwd() {
return storagePwd;
}
@@ -61,4 +65,12 @@ public class Net_CreateAuthSession_Request extends NetRequest {
public void setSignatureB64(String signatureB64) {
this.signatureB64 = signatureB64;
}
}
public String getClientInfo() {
return clientInfo;
}
public void setClientInfo(String clientInfo) {
this.clientInfo = clientInfo;
}
}
@@ -6,7 +6,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
* Ответ на CreateAuthSession.
*
* При успехе сервер создаёт запись в active_sessions
* и возвращает идентификатор сессии sessionId.
* и возвращает идентификатор сессии sessionId и секрет сессии sessionPwd.
*
* JSON:
* {
@@ -14,7 +14,8 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
* "requestId": "...",
* "status": 200,
* "payload": {
* "sessionId": "base64-строка-от-32-байт"
* "sessionId": "base64-строка-от-32-байт",
* "sessionPwd": "base64-строка-от-32-байт"
* }
* }
*/
@@ -23,6 +24,9 @@ public class Net_CreateAuthSession_Response extends NetResponse {
/** Идентификатор сессии, base64 от 32 байт. */
private String sessionId;
/** Секрет сессии, base64 от 32 байт. */
private String sessionPwd;
public String getSessionId() {
return sessionId;
}
@@ -30,4 +34,12 @@ public class Net_CreateAuthSession_Response extends NetResponse {
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
}
public String getSessionPwd() {
return sessionPwd;
}
public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd;
}
}
@@ -11,7 +11,8 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
* JSON (payload):
* {
* "sessionId": "base64-id-сессии",
* "sessionPwd": "base64-sessionPwd"
* "sessionPwd": "base64-sessionPwd",
* "clientInfo": "до 50 символов, краткая строка об устройстве"
* }
*/
public class Net_RefreshSession_Request extends NetRequest {
@@ -19,6 +20,12 @@ public class Net_RefreshSession_Request extends NetRequest {
private String sessionId;
private String sessionPwd;
/**
* Краткая строка с информацией об устройстве/клиенте, до 50 символов.
* Например: "PWA/Chrome/Android".
*/
private String clientInfo;
public String getSessionId() {
return sessionId;
}
@@ -34,4 +41,12 @@ public class Net_RefreshSession_Request extends NetRequest {
public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd;
}
}
public String getClientInfo() {
return clientInfo;
}
public void setClientInfo(String clientInfo) {
this.clientInfo = clientInfo;
}
}
@@ -46,7 +46,6 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login);
if (solanaUser == null) {
// TODO позже — запрос в Solana, если не нашли локально
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
@@ -55,23 +54,24 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
);
}
// 3) Заполняем контекст целиком пользователем
// 3) Заполняем контекст пользователем
ctx.setSolanaUser(solanaUser);
// 4) Генерируем надёжный sessionPwd = base64(32 случайных байт)
// 4) Генерируем одноразовый authNonce = base64(32 случайных байт)
byte[] buf = new byte[32];
RANDOM.nextBytes(buf);
String sessionPwd = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
ctx.setSessionPwd(sessionPwd);
// Используем поле sessionPwd в контексте как хранилище challenge (authNonce) до шага 2
ctx.setSessionPwd(authNonce);
// 5) Формируем ответ
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setSessionPwd(sessionPwd);
resp.setAuthNonce(authNonce);
return resp;
}
}
}
@@ -14,8 +14,13 @@ import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.geo.ClientInfoService;
import utils.crypto.Ed25519Util;
import org.eclipse.jetty.websocket.api.Session;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.security.SecureRandom;
@@ -28,20 +33,18 @@ import java.util.Base64;
* - storagePwd (base64 от 32 байт)
* - timeMs (long, мс с 1970-01-01)
* - signatureB64 (подпись Ed25519 над строкой:
* "AUTHORIFICATED:" + timeMs + sessionPwd)
* "AUTHORIFICATED:" + timeMs + authNonce)
* - clientInfo (опционально, до 50 символов)
*
* Параметр sessionPwd клиент получил на шаге 1.
* Для проверки подписи используется pubkey1 (второй публичный ключ пользователя).
*
* Дополнительно:
* - timeMs должен отличаться от текущего времени сервера не более чем на 30 секунд.
* authNonce клиент получил на шаге 1 (AuthChallenge).
*
* При успехе:
* - создаётся запись ActiveSession в БД;
* - генерируется sessionId (base64 от 32 случайных байт);
* - генерируется sessionPwd (base64 от 32 случайных байт);
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
* - pushEndpoint / pushP256dhKey / pushAuthKey остаются пустыми;
* - возвращается sessionId в ответе.
* - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage;
* - возвращается sessionId и sessionPwd в ответе.
*/
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
@@ -107,7 +110,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
long timeMs = req.getTimeMs();
long nowMs = System.currentTimeMillis();
// Проверка, что время клиента не отличается от времени сервера больше чем на 30 секунд
long diff = Math.abs(nowMs - timeMs);
if (diff > ALLOWED_SKEW_MS) {
return NetExceptionResponseFactory.error(
@@ -118,6 +120,12 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
);
}
// Короткая строка clientInfo от клиента (до 50 символов)
String clientInfoFromClient = req.getClientInfo();
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
}
// --- выбираем публичный ключ pubkey1 ---
String pubKeyB64 = user.getDeviceKey();
if (pubKeyB64 == null || pubKeyB64.isBlank()) {
@@ -143,8 +151,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
);
}
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + sessionPwd ---
String preimageStr = "AUTHORIFICATED:" + timeMs + ctx.getSessionPwd();
// --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 ---
String authNonce = ctx.getSessionPwd();
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + authNonce ---
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
@@ -157,25 +168,47 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
);
}
// --- создаём уникальный sessionId (base64 от 32 байт) и записываем в БД ---
// --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId ---
String newSessionPwd = generateRandomSecret();
String sessionId = generateRandomSessionId();
long now = System.currentTimeMillis();
// --- Сбор данных о клиенте (IP, UA, язык) ---
Session wsSession = ctx.getWsSession();
String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
String clientIp = "";
if (wsSession != null) {
SocketAddress rawAddr = wsSession.getRemoteAddress();
if (rawAddr instanceof InetSocketAddress inet) {
if (inet.getAddress() != null) {
clientIp = inet.getAddress().getHostAddress();
}
}
}
// TODO и сдесь тоже переписываем получение ИП адреса на стандартный метод и тоже дёргаем запрос геолокации который никуда не сохраняем просто что бы он в кэш сервера попал
// --- создаём запись ActiveSession и сохраняем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
String sessionId;
ActiveSession activeSession;
try {
sessionId = generateRandomSessionId();
long now = System.currentTimeMillis();
activeSession = new ActiveSession(
sessionId,
loginId,
ctx.getSessionPwd(),
newSessionPwd, // настоящий секрет сессии
storagePwd,
now,
now,
null, // pushEndpoint
null, // pushP256dhKey
null // pushAuthKey
null, // pushEndpoint
null, // pushP256dhKey
null, // pushAuthKey
clientIp,
clientInfoFromClient,
clientInfoFromRequest,
userLanguage
);
dao.insert(activeSession);
@@ -192,9 +225,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
// --- обновляем контекст ---
ctx.setActiveSession(activeSession);
ctx.setSessionId(sessionId);
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии, а не authNonce
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
// Регистрируем это подключение в глобальном реестре активных соединений
ActiveConnectionsRegistry.getInstance().register(ctx);
// --- формируем ответ ---
@@ -202,7 +235,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setSessionId(sessionId); // попадёт в payload.sessionId
resp.setSessionId(sessionId);
resp.setSessionPwd(newSessionPwd);
return resp;
}
@@ -214,4 +248,13 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
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);
}
}
@@ -15,6 +15,7 @@ import shine.db.dao.ActiveSessionsDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.geo.ClientInfoService;
import java.sql.SQLException;
@@ -24,19 +25,23 @@ import java.sql.SQLException;
* При успешной проверке sessionId + sessionPwd:
* - подтягивает пользователя по loginId из сессии;
* - заполняет ConnectionContext;
* - обновляет lastAuthirificatedAtMs в БД на текущее время;
* - обновляет 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 NetResponse handle(NetRequest 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(
@@ -89,7 +94,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
);
}
// --- достаём пользователя по loginId из сессии ---
// --- вытаскиваем пользователя по loginId ---
SolanaUser solanaUser = null;
long loginId = session.getLoginId();
try {
@@ -114,7 +119,43 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
);
}
// Всё хорошо — обновляем контекст соединения
// --- собираем данные о клиенте из WebSocket-запроса ---
String clientIp = null;
String clientInfoFromRequest = null;
String userLanguage = null;
if (ctx != null && ctx.getWsSession() != null) {
clientIp = "8.8.8.8"; //TODO сделать нормальное получение ип адреса
// и сделать запрос геолокации и никуда его не сохранять запрос нужен просто что бы в кэш данные добавилиь если нужно
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(solanaUser);
@@ -126,15 +167,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
ActiveConnectionsRegistry.getInstance().register(ctx);
}
// Обновляем lastAuthirificatedAtMs в БД
try {
long nowMs = System.currentTimeMillis();
sessionsDao.updateLastAuthirificatedAtMs(sessionId, nowMs);
} catch (SQLException e) {
log.error("Ошибка БД при обновлении lastAuthirificatedAtMs для sessionId={}", sessionId, e);
}
// Возвращаем OK + storagePwd
// --- ответ OK + storagePwd ---
Net_RefreshSession_Response resp = new Net_RefreshSession_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
@@ -142,4 +175,13 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
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;
}
}