From 2ed4f6d666d40448883a996ab94ea37aa3a8fe31479618b5ef25a01ffe778e59 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 9 Dec 2025 19:12:37 +0300 Subject: [PATCH] 09 12 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В черновую переделал авторификацию --- .../java/shine/db/dao/ActiveSessionsDAO.java | 78 +++++++----- .../java/shine/db/entities/ActiveSession.java | 84 ++++++++----- .../JSON/ActiveConnectionsRegistry.java | 30 ++--- .../ws_protocol/JSON/ConnectionContext.java | 13 +- .../Auth/NetAuthSessionNewStep1Request.java | 29 +++++ .../Auth/NetAuthSessionNewStep1Response.java | 21 ++++ .../Auth/NetAuthSessionNewStep2Request.java | 52 ++++---- .../Auth/NetAuthSessionNewStep2Response.java | 16 ++- .../Auth/NetSessionRefreshRequest.java | 15 ++- .../Auth/NetSessionRefreshResponse.java | 27 +++- .../auth/NetAuthSessionNewStep1Handler.java | 10 +- .../auth/NetAuthSessionNewStep2Handler.java | 116 +++++++++++------- .../auth/NetSessionRefreshHandler.java | 40 ++++-- 13 files changed, 354 insertions(+), 177 deletions(-) diff --git a/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java b/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java index cd9603b..d34cd61 100644 --- a/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java @@ -5,8 +5,7 @@ import shine.db.entities.ActiveSession; import java.sql.*; -/** Здесь мы хрним данные об активных сессиях пользователя (для wss соединений) */ - +/** Здесь мы храним данные об активных сессиях пользователя (для wss соединений). */ public final class ActiveSessionsDAO { private static volatile ActiveSessionsDAO instance; @@ -30,37 +29,40 @@ public final class ActiveSessionsDAO { String sql = """ INSERT INTO active_sessions ( sessionId, - session_pwd, loginId, - time_ms, - pubkey_num, + session_pwd, + storage_pwd, + session_created_ms, + last_auth_ms, push_endpoint, push_p256dh_key, push_auth_key - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - ps.setLong(1, session.getSessionId()); - ps.setString(2, session.getSessionPwd()); - ps.setLong(3, session.getLoginId()); - ps.setLong(4, session.getTimeMs()); - ps.setInt(5, session.getPubkeyNum()); - ps.setString(6, session.getPushEndpoint()); - ps.setString(7, session.getPushP256dhKey()); - ps.setString(8, session.getPushAuthKey()); + ps.setString(1, session.getSessionId()); + ps.setLong(2, session.getLoginId()); + ps.setString(3, session.getSessionPwd()); + ps.setString(4, session.getStoragePwd()); + ps.setLong(5, session.getSessionCreatedAtMs()); + ps.setLong(6, session.getLastAuthirificatedAtMs()); + ps.setString(7, session.getPushEndpoint()); + ps.setString(8, session.getPushP256dhKey()); + ps.setString(9, session.getPushAuthKey()); ps.executeUpdate(); } } - public ActiveSession getBySessionId(long sessionId) throws SQLException { + public ActiveSession getBySessionId(String sessionId) throws SQLException { String sql = """ SELECT sessionId, - session_pwd, loginId, - time_ms, - pubkey_num, + session_pwd, + storage_pwd, + session_created_ms, + last_auth_ms, push_endpoint, push_p256dh_key, push_auth_key @@ -69,7 +71,7 @@ public final class ActiveSessionsDAO { """; try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - ps.setLong(1, sessionId); + ps.setString(1, sessionId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { return null; @@ -83,31 +85,51 @@ public final class ActiveSessionsDAO { * Удаление записи по sessionId. * Если записи нет — просто ничего не удалит (0 строк). */ - public void deleteBySessionId(long sessionId) throws SQLException { + public void deleteBySessionId(String sessionId) throws SQLException { String sql = "DELETE FROM active_sessions WHERE sessionId = ?"; try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - ps.setLong(1, sessionId); + ps.setString(1, sessionId); + ps.executeUpdate(); + } + } + + /** + * Обновить поле last_auth_ms (lastAuthirificatedAtMs) для конкретной сессии. + * Остальные поля записи не меняются. + */ + public void updateLastAuthirificatedAtMs(String sessionId, long newTimeMs) throws SQLException { + String sql = """ + UPDATE active_sessions + SET last_auth_ms = ? + WHERE sessionId = ? + """; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setLong(1, newTimeMs); + ps.setString(2, sessionId); ps.executeUpdate(); } } private ActiveSession mapRow(ResultSet rs) throws SQLException { - long sessionId = rs.getLong("sessionId"); - String sessionPwd = rs.getString("session_pwd"); + String sessionId = rs.getString("sessionId"); long loginId = rs.getLong("loginId"); - long timeMs = rs.getLong("time_ms"); - short pubkeyNum = (short) rs.getInt("pubkey_num"); + String sessionPwd = rs.getString("session_pwd"); + String storagePwd = rs.getString("storage_pwd"); + long sessionCreatedMs = rs.getLong("session_created_ms"); + long lastAuthMs = rs.getLong("last_auth_ms"); String pushEndpoint = rs.getString("push_endpoint"); String pushP256dhKey = rs.getString("push_p256dh_key"); String pushAuthKey = rs.getString("push_auth_key"); return new ActiveSession( sessionId, - sessionPwd, loginId, - timeMs, - pubkeyNum, + sessionPwd, + storagePwd, + sessionCreatedMs, + lastAuthMs, pushEndpoint, pushP256dhKey, pushAuthKey diff --git a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java b/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java index 4a541e7..ea7a0ba 100644 --- a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java +++ b/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java @@ -1,12 +1,27 @@ package shine.db.entities; +/** + * ActiveSession — запись об активной сессии пользователя. + * + * Поля: + * - sessionId – строка (base64 от 32 байт) + * - loginId – long + * - sessionPwd – строка (секрет шага 1) + * - storagePwd – строка (секрет клиента для хранения данных) + * - sessionCreatedAtMs – long (время создания) + * - lastAuthirificatedAtMs – long (последнее подтверждение/refresh) + * - pushEndpoint – строка (WebPush, пока null/пусто) + * - pushP256dhKey – строка (WebPush, пока null/пусто) + * - pushAuthKey – строка (WebPush, пока null/пусто) + */ public class ActiveSession { - private long sessionId; - private String sessionPwd; + private String sessionId; private long loginId; - private long timeMs; // время в мс - private short pubkeyNum; + private String sessionPwd; + private String storagePwd; + private long sessionCreatedAtMs; + private long lastAuthirificatedAtMs; private String pushEndpoint; private String pushP256dhKey; private String pushAuthKey; @@ -14,68 +29,71 @@ public class ActiveSession { public ActiveSession() { } - public ActiveSession(long sessionId, - String sessionPwd, + public ActiveSession(String sessionId, long loginId, - long timeMs, - short pubkeyNum, + String sessionPwd, + String storagePwd, + long sessionCreatedAtMs, + long lastAuthirificatedAtMs, String pushEndpoint, String pushP256dhKey, String pushAuthKey) { this.sessionId = sessionId; - this.sessionPwd = sessionPwd; this.loginId = loginId; - this.timeMs = timeMs; - this.pubkeyNum = pubkeyNum; + this.sessionPwd = sessionPwd; + this.storagePwd = storagePwd; + this.sessionCreatedAtMs = sessionCreatedAtMs; + this.lastAuthirificatedAtMs = lastAuthirificatedAtMs; this.pushEndpoint = pushEndpoint; this.pushP256dhKey = pushP256dhKey; this.pushAuthKey = pushAuthKey; } - public long getSessionId() { + public String getSessionId() { return sessionId; } - - public void setSessionId(long sessionId) { + public void setSessionId(String sessionId) { this.sessionId = sessionId; } - public String getSessionPwd() { - return sessionPwd; - } - - public void setSessionPwd(String sessionPwd) { - this.sessionPwd = sessionPwd; - } - public long getLoginId() { return loginId; } - public void setLoginId(long loginId) { this.loginId = loginId; } - public long getTimeMs() { - return timeMs; + public String getSessionPwd() { + return sessionPwd; + } + public void setSessionPwd(String sessionPwd) { + this.sessionPwd = sessionPwd; } - public void setTimeMs(long timeMs) { - this.timeMs = timeMs; + public String getStoragePwd() { + return storagePwd; + } + public void setStoragePwd(String storagePwd) { + this.storagePwd = storagePwd; } - public short getPubkeyNum() { - return pubkeyNum; + public long getSessionCreatedAtMs() { + return sessionCreatedAtMs; + } + public void setSessionCreatedAtMs(long sessionCreatedAtMs) { + this.sessionCreatedAtMs = sessionCreatedAtMs; } - public void setPubkeyNum(short pubkeyNum) { - this.pubkeyNum = pubkeyNum; + public long getLastAuthirificatedAtMs() { + return lastAuthirificatedAtMs; + } + public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) { + this.lastAuthirificatedAtMs = lastAuthirificatedAtMs; } public String getPushEndpoint() { return pushEndpoint; } - public void setPushEndpoint(String pushEndpoint) { this.pushEndpoint = pushEndpoint; } @@ -83,7 +101,6 @@ public class ActiveSession { public String getPushP256dhKey() { return pushP256dhKey; } - public void setPushP256dhKey(String pushP256dhKey) { this.pushP256dhKey = pushP256dhKey; } @@ -91,7 +108,6 @@ public class ActiveSession { public String getPushAuthKey() { return pushAuthKey; } - public void setPushAuthKey(String pushAuthKey) { this.pushAuthKey = pushAuthKey; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java index d8885c5..1970304 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ActiveConnectionsRegistry.java @@ -6,20 +6,19 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * Реестр активных подключений (только авторизованные). - *. + * * Позволяет: * - получить ConnectionContext по sessionId; * - получить все активные подключения пользователя по loginId; * - удалить подключение при закрытии WebSocket. - *. + * * найти все подключения пользователя: - * var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId); - *. - * найти конкретное подключение по sessionId: - * ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId); - * Session ws = ctx != null ? ctx.getWsSession() : null; + * var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId); + * + * найти конкретное подключение по sessionId: + * ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId); + * Session ws = ctx != null ? ctx.getWsSession() : null; */ - public final class ActiveConnectionsRegistry { private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry(); @@ -32,8 +31,8 @@ public final class ActiveConnectionsRegistry { // singleton } - // sessionId -> ConnectionContext - private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); + // sessionId (String) -> ConnectionContext + private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); // loginId -> множество ConnectionContext для этого пользователя private final ConcurrentHashMap> byLoginId = new ConcurrentHashMap<>(); @@ -45,7 +44,7 @@ public final class ActiveConnectionsRegistry { public void register(ConnectionContext ctx) { if (ctx == null) return; - Long sessionId = ctx.getSessionId(); + String sessionId = ctx.getSessionId(); Long loginId = ctx.getLoginId(); if (sessionId == null || loginId == null) { @@ -65,7 +64,7 @@ public final class ActiveConnectionsRegistry { public void remove(ConnectionContext ctx) { if (ctx == null) return; - Long sessionId = ctx.getSessionId(); + String sessionId = ctx.getSessionId(); Long loginId = ctx.getLoginId(); if (sessionId != null) { @@ -86,7 +85,9 @@ public final class ActiveConnectionsRegistry { /** * Удалить подключение по sessionId. */ - public void removeBySessionId(long sessionId) { + public void removeBySessionId(String sessionId) { + if (sessionId == null) return; + ConnectionContext ctx = bySessionId.remove(sessionId); if (ctx != null) { Long loginId = ctx.getLoginId(); @@ -105,7 +106,8 @@ public final class ActiveConnectionsRegistry { /** * Получить контекст по sessionId. */ - public ConnectionContext getBySessionId(long sessionId) { + public ConnectionContext getBySessionId(String sessionId) { + if (sessionId == null) return null; return bySessionId.get(sessionId); } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index e38bb57..dac86b3 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -20,7 +20,14 @@ public class ConnectionContext { // Активная сессия из БД (active_sessions) private ActiveSession activeSession; - private Long sessionId; + /** + * Идентификатор сессии — base64-строка от 32 байт. + */ + private String sessionId; + + /** + * Временный секрет шага 1, который используется на шаге 2 и хранится в БД. + */ private String sessionPwd; private int authenticationStatus = AUTH_STATUS_NONE; @@ -71,11 +78,11 @@ public class ConnectionContext { // --- sessionId / sessionPwd --- - public Long getSessionId() { + public String getSessionId() { return sessionId; } - public void setSessionId(Long sessionId) { + public void setSessionId(String sessionId) { this.sessionId = sessionId; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Request.java index 3407c8a..0c924e5 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Request.java @@ -2,7 +2,36 @@ package server.logic.ws_protocol.JSON.entyties.Auth; import server.logic.ws_protocol.JSON.entyties.NetRequest; +/** + * Шаг 1 авторизации: запрос выдачи временного пароля сессии (sessionPwd). + * + * Клиент по логину просит сервер сгенерировать случайный секрет sessionPwd, + * который будет использован на втором шаге при подписи. + * + * Формат входящего JSON: + * { + * "op": "AuthSessionNewStep1", + * "requestId": "...", + * "payload": { + * "login": "someLogin" + * } + * } + * + * Формат успешного ответа: + * { + * "op": "AuthSessionNewStep1", + * "requestId": "...", + * "status": 200, + * "payload": { + * "sessionPwd": "base64-строка-от-32-байт" + * } + * } + */ public class NetAuthSessionNewStep1Request extends NetRequest { + + /** + * Логин пользователя, для которого запускается авторизация. + */ private String login; public String getLogin() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Response.java index d865037..c5756b2 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep1Response.java @@ -2,7 +2,28 @@ package server.logic.ws_protocol.JSON.entyties.Auth; import server.logic.ws_protocol.JSON.entyties.NetResponse; +/** + * Ответ на AuthSessionNewStep1. + * + * При успехе сервер возвращает временный секрет sessionPwd, + * который клиент обязан использовать на втором шаге при формировании подписи. + * + * JSON: + * { + * "op": "AuthSessionNewStep1", + * "requestId": "...", + * "status": 200, + * "payload": { + * "sessionPwd": "base64-строка-от-32-байт" + * } + * } + */ public class NetAuthSessionNewStep1Response extends NetResponse { + + /** + * Временный секрет, сгенерированный сервером. + * Строка — это base64-представление 32 случайных байт. + */ private String sessionPwd; public String getSessionPwd() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java index bf1d9d2..da97938 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java @@ -3,39 +3,47 @@ package server.logic.ws_protocol.JSON.entyties.Auth; import server.logic.ws_protocol.JSON.entyties.NetRequest; /** - * Шаг 2 авторизации: клиент подтверждает владение ключом. - *. - * JSON: + * Шаг 2 авторизации: подтверждение владения ключом и установка сессии. + * + * Клиент: + * 1) получает от сервера sessionPwd на шаге 1; + * 2) генерирует свой StoragePwd (base64 от 32 байт); + * 3) формирует строку для подписи: + * "AUTHORIFICATED:" + timeMs + sessionPwd + * 4) подписывает эту строку своим приватным ключом (pubkey1), + * отправляет подпись и StoragePwd на сервер. + * + * Формат входящего JSON: * { * "op": "AuthSessionNewStep2", * "requestId": "...", - * "loginId": 100211, - * "sigNum": 0, // номер подписи: 0 или 1 - * "timeMs": 1733310000000, // время в миллисекундах с 1970-01-01 - * "signatureB64": "..." // подпись base64 от строки loginId+timeMs+sessionPwd + * "payload": { + * "storagePwd": "base64-строка-от-32-байт", + * "timeMs": 1733310000000, + * "signatureB64": "base64-подпись-Ed25519" + * } * } + * + * При успешной проверке подписи сервер создаёт запись в active_sessions + * и возвращает sessionId (base64-строка от 32 байт). */ public class NetAuthSessionNewStep2Request extends NetRequest { - private long loginId; - private int sigNum; // 0 или 1 - private long timeMs; // миллисекунды с 1970 + /** Клиентский пароль для хранения данных (base64 от 32 байт). */ + private String storagePwd; + + /** Время на стороне клиента (мс с 1970-01-01). */ + private long timeMs; + + /** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + sessionPwd (base64). */ private String signatureB64; - public long getLoginId() { - return loginId; + public String getStoragePwd() { + return storagePwd; } - public void setLoginId(long loginId) { - this.loginId = loginId; - } - - public int getSigNum() { - return sigNum; - } - - public void setSigNum(int sigNum) { - this.sigNum = sigNum; + public void setStoragePwd(String storagePwd) { + this.storagePwd = storagePwd; } public long getTimeMs() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java index 2d52b6c..841d2cd 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java @@ -4,26 +4,30 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; /** * Ответ на AuthSessionNewStep2. - *. - * Успешный JSON: + * + * При успехе сервер создаёт запись в active_sessions + * и возвращает идентификатор сессии sessionId. + * + * JSON: * { * "op": "AuthSessionNewStep2", * "requestId": "...", * "status": 200, * "payload": { - * "sessionId": 1234567890 + * "sessionId": "base64-строка-от-32-байт" * } * } */ public class NetAuthSessionNewStep2Response extends NetResponse { - private Long sessionId; + /** Идентификатор сессии, base64 от 32 байт. */ + private String sessionId; - public Long getSessionId() { + public String getSessionId() { return sessionId; } - public void setSessionId(Long sessionId) { + public void setSessionId(String sessionId) { this.sessionId = sessionId; } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshRequest.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshRequest.java index 3ed2fa9..36f688f 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshRequest.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshRequest.java @@ -4,23 +4,26 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest; /** * Запрос SessionRefresh. - *. + * + * Используется для повторного входа без повторной подписи: + * клиент хранит sessionId и sessionPwd, которые получил на шаге 2. + * * JSON (payload): * { - * "sessionId": 123, - * "sessionPwd": "abcd..." + * "sessionId": "base64-id-сессии", + * "sessionPwd": "base64-sessionPwd" * } */ public class NetSessionRefreshRequest extends NetRequest { - private long sessionId; + private String sessionId; private String sessionPwd; - public long getSessionId() { + public String getSessionId() { return sessionId; } - public void setSessionId(long sessionId) { + public void setSessionId(String sessionId) { this.sessionId = sessionId; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshResponse.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshResponse.java index 2f8db44..a637314 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshResponse.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetSessionRefreshResponse.java @@ -4,9 +4,30 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; /** * Успешный ответ на SessionRefresh. - *. - * Дополнительных полей нет, достаточно status=200 и (опционально) пустого payload. + * + * Дополнительно к статусу 200 сервер возвращает storagePwd, + * чтобы клиент мог восстановить/синхронизировать локальное хранилище. + * + * JSON: + * { + * "op": "SessionRefresh", + * "requestId": "...", + * "status": 200, + * "payload": { + * "storagePwd": "base64-строка-от-32-байт" + * } + * } */ public class NetSessionRefreshResponse extends NetResponse { - // Ничего дополнительного, вся информация в status. + + /** Пароль хранилища, сохранённый в сессии (storagePwd). */ + private String storagePwd; + + public String getStoragePwd() { + return storagePwd; + } + + public void setStoragePwd(String storagePwd) { + this.storagePwd = storagePwd; + } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java index 8a2f87e..b1bb5bf 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java @@ -11,6 +11,7 @@ import shine.db.dao.SolanaUsersDAO; import shine.db.entities.SolanaUser; import java.security.SecureRandom; +import java.util.Base64; public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { @@ -57,9 +58,10 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { // 3) Заполняем контекст целиком пользователем ctx.setSolanaUser(solanaUser); - // 4) Генерируем надёжный sessionPwd - String sessionPwd = Long.toHexString(System.nanoTime()) + - Long.toHexString(RANDOM.nextLong()); + // 4) Генерируем надёжный sessionPwd = base64(32 случайных байт) + byte[] buf = new byte[32]; + RANDOM.nextBytes(buf); + String sessionPwd = Base64.getUrlEncoder().withoutPadding().encodeToString(buf); ctx.setSessionPwd(sessionPwd); @@ -68,7 +70,7 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setSessionPwd(sessionPwd); // 🔴 Больше не трогаем payload + resp.setSessionPwd(sessionPwd); return resp; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java index 07ab1c4..44eddb0 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java @@ -18,22 +18,38 @@ import utils.crypto.Ed25519Util; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.security.SecureRandom; import java.util.Base64; -import java.util.concurrent.ThreadLocalRandom; /** * Шаг 2 авторизации: проверка подписи и создание сессии. - *. - * Клиент присылает: - * - loginId - * - sigNum (0 или 1) - * - timeMs - * - signatureB64 от строки (loginId + timeMs + sessionPwd) + * + * Клиент присылает в payload: + * - storagePwd (base64 от 32 байт) + * - timeMs (long, мс с 1970-01-01) + * - signatureB64 (подпись Ed25519 над строкой: + * "AUTHORIFICATED:" + timeMs + sessionPwd) + * + * Параметр sessionPwd клиент получил на шаге 1. + * Для проверки подписи используется pubkey1 (второй публичный ключ пользователя). + * + * Дополнительно: + * - timeMs должен отличаться от текущего времени сервера не более чем на 30 секунд. + * + * При успехе: + * - создаётся запись ActiveSession в БД; + * - генерируется sessionId (base64 от 32 случайных байт); + * - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время; + * - pushEndpoint / pushP256dhKey / pushAuthKey остаются пустыми; + * - возвращается sessionId в ответе. */ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(NetAuthSessionNewStep2Handler.class); + private static final SecureRandom RANDOM = new SecureRandom(); + private static final long ALLOWED_SKEW_MS = 30_000L; + @Override public NetResponse handle(NetRequest baseReq, ConnectionContext ctx) throws Exception { NetAuthSessionNewStep2Request req = (NetAuthSessionNewStep2Request) baseReq; @@ -58,25 +74,23 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { } SolanaUser user = ctx.getSolanaUser(); - long reqLoginId = req.getLoginId(); - Long ctxLoginId = user.getLoginId(); - - if (ctxLoginId == null || ctxLoginId != reqLoginId) { + Long loginId = user.getLoginId(); + if (loginId == null) { return NetExceptionResponseFactory.error( req, - WireCodes.Status.UNVERIFIED, - "LOGIN_ID_MISMATCH", - "loginId в запросе не совпадает с пользователем из шага 1" + WireCodes.Status.SERVER_DATA_ERROR, + "NO_LOGIN_ID", + "Для пользователя не задан loginId в БД" ); } - int sigNum = req.getSigNum(); - if (sigNum != 0 && sigNum != 1) { + String storagePwd = req.getStoragePwd(); + if (storagePwd == null || storagePwd.isBlank()) { return NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, - "BAD_SIG_NUM", - "Номер подписи должен быть 0 или 1" + "EMPTY_STORAGE_PWD", + "Пустой storagePwd" ); } @@ -90,14 +104,28 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { ); } - // --- выбираем публичный ключ по sigNum --- - String pubKeyB64 = (sigNum == 0) ? user.getPubkey0() : user.getPubkey1(); + long timeMs = req.getTimeMs(); + long nowMs = System.currentTimeMillis(); + + // Проверка, что время клиента не отличается от времени сервера больше чем на 30 секунд + long diff = Math.abs(nowMs - timeMs); + if (diff > ALLOWED_SKEW_MS) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "TIME_SKEW", + "Время клиента отличается от сервера более чем на 30 секунд" + ); + } + + // --- выбираем публичный ключ pubkey1 --- + String pubKeyB64 = user.getPubkey1(); if (pubKeyB64 == null || pubKeyB64.isBlank()) { return NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, - "NO_PUBKEY", - "Отсутствует публичный ключ для выбранного номера подписи" + "NO_PUBKEY1", + "Отсутствует публичный ключ pubkey1 для пользователя" ); } @@ -115,9 +143,8 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { ); } - // --- собираем строку для подписи: loginId + timeMs + sessionPwd --- - long timeMs = req.getTimeMs(); - String preimageStr = String.valueOf(reqLoginId) + timeMs + ctx.getSessionPwd(); + // --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + sessionPwd --- + String preimageStr = "AUTHORIFICATED:" + timeMs + ctx.getSessionPwd(); byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); @@ -130,29 +157,30 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { ); } - // --- создаём уникальный sessionId и записываем в БД --- + // --- создаём уникальный sessionId (base64 от 32 байт) и записываем в БД --- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); - long sessionId; + String sessionId; ActiveSession activeSession; try { - sessionId = generateUniqueSessionId(dao); - long nowMs = System.currentTimeMillis(); + sessionId = generateRandomSessionId(); + long now = System.currentTimeMillis(); activeSession = new ActiveSession( sessionId, + loginId, ctx.getSessionPwd(), - reqLoginId, - nowMs, - (short) sigNum, // pubkeyNum - null, // pushEndpoint - null, // pushP256dhKey - null // pushAuthKey + storagePwd, + now, + now, + null, // pushEndpoint + null, // pushP256dhKey + null // pushAuthKey ); dao.insert(activeSession); } catch (SQLException e) { - log.error("Ошибка БД при создании новой сессии для loginId={}", reqLoginId, e); + log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, @@ -166,7 +194,6 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { ctx.setSessionId(sessionId); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); - ActiveConnectionsRegistry.getInstance().removeBySessionId(sessionId); // га всякий случай предварительно удаляем что бы точно небыло дублирования активной сессии // Регистрируем это подключение в глобальном реестре активных соединений ActiveConnectionsRegistry.getInstance().register(ctx); @@ -180,16 +207,11 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { } /** - * Генерация уникального sessionId с проверкой в БД. + * Генерация случайного sessionId: base64-строка от 32 байт. */ - private long generateUniqueSessionId(ActiveSessionsDAO dao) throws SQLException { - for (int i = 0; i < 10; i++) { - long candidate = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - ActiveSession existing = dao.getBySessionId(candidate); - if (existing == null) { - return candidate; - } - } - throw new SQLException("Не удалось сгенерировать уникальный sessionId за разумное число попыток"); + private String generateRandomSessionId() { + byte[] buf = new byte[32]; + RANDOM.nextBytes(buf); + return Base64.getUrlEncoder().withoutPadding().encodeToString(buf); } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java index 8c63a2a..2f00e9d 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java @@ -20,6 +20,12 @@ import java.sql.SQLException; /** * Хэндлер SessionRefresh. + * + * При успешной проверке sessionId + sessionPwd: + * - подтягивает пользователя по loginId из сессии; + * - заполняет ConnectionContext; + * - обновляет lastAuthirificatedAtMs в БД на текущее время; + * - возвращает storagePwd в payload. */ public class NetSessionRefreshHandler implements JsonMessageHandler { @@ -29,9 +35,18 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception { NetSessionRefreshRequest req = (NetSessionRefreshRequest) request; - long sessionId = req.getSessionId(); + String sessionId = req.getSessionId(); String sessionPwd = req.getSessionPwd(); + 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, @@ -76,13 +91,10 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { // --- достаём пользователя по loginId из сессии --- SolanaUser solanaUser = null; - Long loginId = null; + long loginId = session.getLoginId(); try { - loginId = session.getLoginId(); - if (loginId != null) { - SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); - solanaUser = usersDao.getByLoginId(loginId); - } + SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); + solanaUser = usersDao.getByLoginId(loginId); } catch (SQLException e) { log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); return NetExceptionResponseFactory.error( @@ -93,7 +105,7 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { ); } - if (loginId != null && solanaUser == null) { + if (solanaUser == null) { return NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, @@ -110,16 +122,24 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { ctx.setSessionPwd(sessionPwd); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); - ActiveConnectionsRegistry.getInstance().removeBySessionId(sessionId); // на всякий случай удаляем что бы точно небыло повторов // Регистрируем это подключение в глобальном реестре активных соединений ActiveConnectionsRegistry.getInstance().register(ctx); } - // И возвращаем OK без доп. полей (payload будет {}). + // Обновляем lastAuthirificatedAtMs в БД + try { + long nowMs = System.currentTimeMillis(); + sessionsDao.updateLastAuthirificatedAtMs(sessionId, nowMs); + } catch (SQLException e) { + log.error("Ошибка БД при обновлении lastAuthirificatedAtMs для sessionId={}", sessionId, e); + } + + // Возвращаем OK + storagePwd NetSessionRefreshResponse resp = new NetSessionRefreshResponse(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); + resp.setStoragePwd(session.getStoragePwd()); return resp; } }