В черновую переделал авторификацию
This commit is contained in:
AidarKC 2025-12-09 19:12:37 +03:00
parent 2b5fa16824
commit 2ed4f6d666
13 changed files with 354 additions and 177 deletions

View File

@ -5,8 +5,7 @@ import shine.db.entities.ActiveSession;
import java.sql.*; import java.sql.*;
/** Здесь мы хрним данные об активных сессиях пользователя (для wss соединений) */ /** Здесь мы храним данные об активных сессиях пользователя (для wss соединений). */
public final class ActiveSessionsDAO { public final class ActiveSessionsDAO {
private static volatile ActiveSessionsDAO instance; private static volatile ActiveSessionsDAO instance;
@ -30,37 +29,40 @@ public final class ActiveSessionsDAO {
String sql = """ String sql = """
INSERT INTO active_sessions ( INSERT INTO active_sessions (
sessionId, sessionId,
session_pwd,
loginId, loginId,
time_ms, session_pwd,
pubkey_num, storage_pwd,
session_created_ms,
last_auth_ms,
push_endpoint, push_endpoint,
push_p256dh_key, push_p256dh_key,
push_auth_key push_auth_key
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, session.getSessionId()); ps.setString(1, session.getSessionId());
ps.setString(2, session.getSessionPwd()); ps.setLong(2, session.getLoginId());
ps.setLong(3, session.getLoginId()); ps.setString(3, session.getSessionPwd());
ps.setLong(4, session.getTimeMs()); ps.setString(4, session.getStoragePwd());
ps.setInt(5, session.getPubkeyNum()); ps.setLong(5, session.getSessionCreatedAtMs());
ps.setString(6, session.getPushEndpoint()); ps.setLong(6, session.getLastAuthirificatedAtMs());
ps.setString(7, session.getPushP256dhKey()); ps.setString(7, session.getPushEndpoint());
ps.setString(8, session.getPushAuthKey()); ps.setString(8, session.getPushP256dhKey());
ps.setString(9, session.getPushAuthKey());
ps.executeUpdate(); ps.executeUpdate();
} }
} }
public ActiveSession getBySessionId(long sessionId) throws SQLException { public ActiveSession getBySessionId(String sessionId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, sessionId,
session_pwd,
loginId, loginId,
time_ms, session_pwd,
pubkey_num, storage_pwd,
session_created_ms,
last_auth_ms,
push_endpoint, push_endpoint,
push_p256dh_key, push_p256dh_key,
push_auth_key push_auth_key
@ -69,7 +71,7 @@ public final class ActiveSessionsDAO {
"""; """;
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
ps.setLong(1, sessionId); ps.setString(1, sessionId);
try (ResultSet rs = ps.executeQuery()) { try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) { if (!rs.next()) {
return null; return null;
@ -83,31 +85,51 @@ public final class ActiveSessionsDAO {
* Удаление записи по sessionId. * Удаление записи по sessionId.
* Если записи нет просто ничего не удалит (0 строк). * Если записи нет просто ничего не удалит (0 строк).
*/ */
public void deleteBySessionId(long sessionId) throws SQLException { public void deleteBySessionId(String sessionId) throws SQLException {
String sql = "DELETE FROM active_sessions WHERE sessionId = ?"; String sql = "DELETE FROM active_sessions WHERE sessionId = ?";
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { 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(); ps.executeUpdate();
} }
} }
private ActiveSession mapRow(ResultSet rs) throws SQLException { private ActiveSession mapRow(ResultSet rs) throws SQLException {
long sessionId = rs.getLong("sessionId"); String sessionId = rs.getString("sessionId");
String sessionPwd = rs.getString("session_pwd");
long loginId = rs.getLong("loginId"); long loginId = rs.getLong("loginId");
long timeMs = rs.getLong("time_ms"); String sessionPwd = rs.getString("session_pwd");
short pubkeyNum = (short) rs.getInt("pubkey_num"); 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 pushEndpoint = rs.getString("push_endpoint");
String pushP256dhKey = rs.getString("push_p256dh_key"); String pushP256dhKey = rs.getString("push_p256dh_key");
String pushAuthKey = rs.getString("push_auth_key"); String pushAuthKey = rs.getString("push_auth_key");
return new ActiveSession( return new ActiveSession(
sessionId, sessionId,
sessionPwd,
loginId, loginId,
timeMs, sessionPwd,
pubkeyNum, storagePwd,
sessionCreatedMs,
lastAuthMs,
pushEndpoint, pushEndpoint,
pushP256dhKey, pushP256dhKey,
pushAuthKey pushAuthKey

View File

@ -1,12 +1,27 @@
package shine.db.entities; 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 { public class ActiveSession {
private long sessionId; private String sessionId;
private String sessionPwd;
private long loginId; private long loginId;
private long timeMs; // время в мс private String sessionPwd;
private short pubkeyNum; private String storagePwd;
private long sessionCreatedAtMs;
private long lastAuthirificatedAtMs;
private String pushEndpoint; private String pushEndpoint;
private String pushP256dhKey; private String pushP256dhKey;
private String pushAuthKey; private String pushAuthKey;
@ -14,68 +29,71 @@ public class ActiveSession {
public ActiveSession() { public ActiveSession() {
} }
public ActiveSession(long sessionId, public ActiveSession(String sessionId,
String sessionPwd,
long loginId, long loginId,
long timeMs, String sessionPwd,
short pubkeyNum, String storagePwd,
long sessionCreatedAtMs,
long lastAuthirificatedAtMs,
String pushEndpoint, String pushEndpoint,
String pushP256dhKey, String pushP256dhKey,
String pushAuthKey) { String pushAuthKey) {
this.sessionId = sessionId; this.sessionId = sessionId;
this.sessionPwd = sessionPwd;
this.loginId = loginId; this.loginId = loginId;
this.timeMs = timeMs; this.sessionPwd = sessionPwd;
this.pubkeyNum = pubkeyNum; this.storagePwd = storagePwd;
this.sessionCreatedAtMs = sessionCreatedAtMs;
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
this.pushEndpoint = pushEndpoint; this.pushEndpoint = pushEndpoint;
this.pushP256dhKey = pushP256dhKey; this.pushP256dhKey = pushP256dhKey;
this.pushAuthKey = pushAuthKey; this.pushAuthKey = pushAuthKey;
} }
public long getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public void setSessionId(String sessionId) {
public void setSessionId(long sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }
public String getSessionPwd() {
return sessionPwd;
}
public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd;
}
public long getLoginId() { public long getLoginId() {
return loginId; return loginId;
} }
public void setLoginId(long loginId) { public void setLoginId(long loginId) {
this.loginId = loginId; this.loginId = loginId;
} }
public long getTimeMs() { public String getSessionPwd() {
return timeMs; return sessionPwd;
}
public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd;
} }
public void setTimeMs(long timeMs) { public String getStoragePwd() {
this.timeMs = timeMs; return storagePwd;
}
public void setStoragePwd(String storagePwd) {
this.storagePwd = storagePwd;
} }
public short getPubkeyNum() { public long getSessionCreatedAtMs() {
return pubkeyNum; return sessionCreatedAtMs;
}
public void setSessionCreatedAtMs(long sessionCreatedAtMs) {
this.sessionCreatedAtMs = sessionCreatedAtMs;
} }
public void setPubkeyNum(short pubkeyNum) { public long getLastAuthirificatedAtMs() {
this.pubkeyNum = pubkeyNum; return lastAuthirificatedAtMs;
}
public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
} }
public String getPushEndpoint() { public String getPushEndpoint() {
return pushEndpoint; return pushEndpoint;
} }
public void setPushEndpoint(String pushEndpoint) { public void setPushEndpoint(String pushEndpoint) {
this.pushEndpoint = pushEndpoint; this.pushEndpoint = pushEndpoint;
} }
@ -83,7 +101,6 @@ public class ActiveSession {
public String getPushP256dhKey() { public String getPushP256dhKey() {
return pushP256dhKey; return pushP256dhKey;
} }
public void setPushP256dhKey(String pushP256dhKey) { public void setPushP256dhKey(String pushP256dhKey) {
this.pushP256dhKey = pushP256dhKey; this.pushP256dhKey = pushP256dhKey;
} }
@ -91,7 +108,6 @@ public class ActiveSession {
public String getPushAuthKey() { public String getPushAuthKey() {
return pushAuthKey; return pushAuthKey;
} }
public void setPushAuthKey(String pushAuthKey) { public void setPushAuthKey(String pushAuthKey) {
this.pushAuthKey = pushAuthKey; this.pushAuthKey = pushAuthKey;
} }

View File

@ -6,20 +6,19 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* Реестр активных подключений (только авторизованные). * Реестр активных подключений (только авторизованные).
*. *
* Позволяет: * Позволяет:
* - получить ConnectionContext по sessionId; * - получить ConnectionContext по sessionId;
* - получить все активные подключения пользователя по loginId; * - получить все активные подключения пользователя по loginId;
* - удалить подключение при закрытии WebSocket. * - удалить подключение при закрытии WebSocket.
*. *
* найти все подключения пользователя: * найти все подключения пользователя:
* var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId); * var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId);
*. *
* найти конкретное подключение по sessionId: * найти конкретное подключение по sessionId:
* ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId); * ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
* Session ws = ctx != null ? ctx.getWsSession() : null; * Session ws = ctx != null ? ctx.getWsSession() : null;
*/ */
public final class ActiveConnectionsRegistry { public final class ActiveConnectionsRegistry {
private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry(); private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
@ -32,8 +31,8 @@ public final class ActiveConnectionsRegistry {
// singleton // singleton
} }
// sessionId -> ConnectionContext // sessionId (String) -> ConnectionContext
private final ConcurrentHashMap<Long, ConnectionContext> bySessionId = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
// loginId -> множество ConnectionContext для этого пользователя // loginId -> множество ConnectionContext для этого пользователя
private final ConcurrentHashMap<Long, Set<ConnectionContext>> byLoginId = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, Set<ConnectionContext>> byLoginId = new ConcurrentHashMap<>();
@ -45,7 +44,7 @@ public final class ActiveConnectionsRegistry {
public void register(ConnectionContext ctx) { public void register(ConnectionContext ctx) {
if (ctx == null) return; if (ctx == null) return;
Long sessionId = ctx.getSessionId(); String sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId(); Long loginId = ctx.getLoginId();
if (sessionId == null || loginId == null) { if (sessionId == null || loginId == null) {
@ -65,7 +64,7 @@ public final class ActiveConnectionsRegistry {
public void remove(ConnectionContext ctx) { public void remove(ConnectionContext ctx) {
if (ctx == null) return; if (ctx == null) return;
Long sessionId = ctx.getSessionId(); String sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId(); Long loginId = ctx.getLoginId();
if (sessionId != null) { if (sessionId != null) {
@ -86,7 +85,9 @@ public final class ActiveConnectionsRegistry {
/** /**
* Удалить подключение по sessionId. * Удалить подключение по sessionId.
*/ */
public void removeBySessionId(long sessionId) { public void removeBySessionId(String sessionId) {
if (sessionId == null) return;
ConnectionContext ctx = bySessionId.remove(sessionId); ConnectionContext ctx = bySessionId.remove(sessionId);
if (ctx != null) { if (ctx != null) {
Long loginId = ctx.getLoginId(); Long loginId = ctx.getLoginId();
@ -105,7 +106,8 @@ public final class ActiveConnectionsRegistry {
/** /**
* Получить контекст по sessionId. * Получить контекст по sessionId.
*/ */
public ConnectionContext getBySessionId(long sessionId) { public ConnectionContext getBySessionId(String sessionId) {
if (sessionId == null) return null;
return bySessionId.get(sessionId); return bySessionId.get(sessionId);
} }

View File

@ -20,7 +20,14 @@ public class ConnectionContext {
// Активная сессия из БД (active_sessions) // Активная сессия из БД (active_sessions)
private ActiveSession activeSession; private ActiveSession activeSession;
private Long sessionId; /**
* Идентификатор сессии base64-строка от 32 байт.
*/
private String sessionId;
/**
* Временный секрет шага 1, который используется на шаге 2 и хранится в БД.
*/
private String sessionPwd; private String sessionPwd;
private int authenticationStatus = AUTH_STATUS_NONE; private int authenticationStatus = AUTH_STATUS_NONE;
@ -71,11 +78,11 @@ public class ConnectionContext {
// --- sessionId / sessionPwd --- // --- sessionId / sessionPwd ---
public Long getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public void setSessionId(Long sessionId) { public void setSessionId(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }

View File

@ -2,7 +2,36 @@ package server.logic.ws_protocol.JSON.entyties.Auth;
import server.logic.ws_protocol.JSON.entyties.NetRequest; 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 { public class NetAuthSessionNewStep1Request extends NetRequest {
/**
* Логин пользователя, для которого запускается авторизация.
*/
private String login; private String login;
public String getLogin() { public String getLogin() {

View File

@ -2,7 +2,28 @@ package server.logic.ws_protocol.JSON.entyties.Auth;
import server.logic.ws_protocol.JSON.entyties.NetResponse; 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 { public class NetAuthSessionNewStep1Response extends NetResponse {
/**
* Временный секрет, сгенерированный сервером.
* Строка это base64-представление 32 случайных байт.
*/
private String sessionPwd; private String sessionPwd;
public String getSessionPwd() { public String getSessionPwd() {

View File

@ -3,39 +3,47 @@ package server.logic.ws_protocol.JSON.entyties.Auth;
import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Шаг 2 авторизации: клиент подтверждает владение ключом. * Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
*. *
* JSON: * Клиент:
* 1) получает от сервера sessionPwd на шаге 1;
* 2) генерирует свой StoragePwd (base64 от 32 байт);
* 3) формирует строку для подписи:
* "AUTHORIFICATED:" + timeMs + sessionPwd
* 4) подписывает эту строку своим приватным ключом (pubkey1),
* отправляет подпись и StoragePwd на сервер.
*
* Формат входящего JSON:
* { * {
* "op": "AuthSessionNewStep2", * "op": "AuthSessionNewStep2",
* "requestId": "...", * "requestId": "...",
* "loginId": 100211, * "payload": {
* "sigNum": 0, // номер подписи: 0 или 1 * "storagePwd": "base64-строка-от-32-байт",
* "timeMs": 1733310000000, // время в миллисекундах с 1970-01-01 * "timeMs": 1733310000000,
* "signatureB64": "..." // подпись base64 от строки loginId+timeMs+sessionPwd * "signatureB64": "base64-подпись-Ed25519"
* }
* } * }
*
* При успешной проверке подписи сервер создаёт запись в active_sessions
* и возвращает sessionId (base64-строка от 32 байт).
*/ */
public class NetAuthSessionNewStep2Request extends NetRequest { public class NetAuthSessionNewStep2Request extends NetRequest {
private long loginId; /** Клиентский пароль для хранения данных (base64 от 32 байт). */
private int sigNum; // 0 или 1 private String storagePwd;
private long timeMs; // миллисекунды с 1970
/** Время на стороне клиента (мс с 1970-01-01). */
private long timeMs;
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + sessionPwd (base64). */
private String signatureB64; private String signatureB64;
public long getLoginId() { public String getStoragePwd() {
return loginId; return storagePwd;
} }
public void setLoginId(long loginId) { public void setStoragePwd(String storagePwd) {
this.loginId = loginId; this.storagePwd = storagePwd;
}
public int getSigNum() {
return sigNum;
}
public void setSigNum(int sigNum) {
this.sigNum = sigNum;
} }
public long getTimeMs() { public long getTimeMs() {

View File

@ -4,26 +4,30 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/** /**
* Ответ на AuthSessionNewStep2. * Ответ на AuthSessionNewStep2.
*. *
* Успешный JSON: * При успехе сервер создаёт запись в active_sessions
* и возвращает идентификатор сессии sessionId.
*
* JSON:
* { * {
* "op": "AuthSessionNewStep2", * "op": "AuthSessionNewStep2",
* "requestId": "...", * "requestId": "...",
* "status": 200, * "status": 200,
* "payload": { * "payload": {
* "sessionId": 1234567890 * "sessionId": "base64-строка-от-32-байт"
* } * }
* } * }
*/ */
public class NetAuthSessionNewStep2Response extends NetResponse { public class NetAuthSessionNewStep2Response extends NetResponse {
private Long sessionId; /** Идентификатор сессии, base64 от 32 байт. */
private String sessionId;
public Long getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public void setSessionId(Long sessionId) { public void setSessionId(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }
} }

View File

@ -4,23 +4,26 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Запрос SessionRefresh. * Запрос SessionRefresh.
*. *
* Используется для повторного входа без повторной подписи:
* клиент хранит sessionId и sessionPwd, которые получил на шаге 2.
*
* JSON (payload): * JSON (payload):
* { * {
* "sessionId": 123, * "sessionId": "base64-id-сессии",
* "sessionPwd": "abcd..." * "sessionPwd": "base64-sessionPwd"
* } * }
*/ */
public class NetSessionRefreshRequest extends NetRequest { public class NetSessionRefreshRequest extends NetRequest {
private long sessionId; private String sessionId;
private String sessionPwd; private String sessionPwd;
public long getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public void setSessionId(long sessionId) { public void setSessionId(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }

View File

@ -4,9 +4,30 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/** /**
* Успешный ответ на SessionRefresh. * Успешный ответ на SessionRefresh.
*. *
* Дополнительных полей нет, достаточно status=200 и (опционально) пустого payload. * Дополнительно к статусу 200 сервер возвращает storagePwd,
* чтобы клиент мог восстановить/синхронизировать локальное хранилище.
*
* JSON:
* {
* "op": "SessionRefresh",
* "requestId": "...",
* "status": 200,
* "payload": {
* "storagePwd": "base64-строка-от-32-байт"
* }
* }
*/ */
public class NetSessionRefreshResponse extends NetResponse { public class NetSessionRefreshResponse extends NetResponse {
// Ничего дополнительного, вся информация в status.
/** Пароль хранилища, сохранённый в сессии (storagePwd). */
private String storagePwd;
public String getStoragePwd() {
return storagePwd;
}
public void setStoragePwd(String storagePwd) {
this.storagePwd = storagePwd;
}
} }

View File

@ -11,6 +11,7 @@ import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUser;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Base64;
public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { public class NetAuthSessionNewStep1Handler implements JsonMessageHandler {
@ -57,9 +58,10 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler {
// 3) Заполняем контекст целиком пользователем // 3) Заполняем контекст целиком пользователем
ctx.setSolanaUser(solanaUser); ctx.setSolanaUser(solanaUser);
// 4) Генерируем надёжный sessionPwd // 4) Генерируем надёжный sessionPwd = base64(32 случайных байт)
String sessionPwd = Long.toHexString(System.nanoTime()) + byte[] buf = new byte[32];
Long.toHexString(RANDOM.nextLong()); RANDOM.nextBytes(buf);
String sessionPwd = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
ctx.setSessionPwd(sessionPwd); ctx.setSessionPwd(sessionPwd);
@ -68,7 +70,7 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler {
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
resp.setSessionPwd(sessionPwd); // 🔴 Больше не трогаем payload resp.setSessionPwd(sessionPwd);
return resp; return resp;
} }

View File

@ -18,22 +18,38 @@ import utils.crypto.Ed25519Util;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.sql.SQLException; import java.sql.SQLException;
import java.security.SecureRandom;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
/** /**
* Шаг 2 авторизации: проверка подписи и создание сессии. * Шаг 2 авторизации: проверка подписи и создание сессии.
*. *
* Клиент присылает: * Клиент присылает в payload:
* - loginId * - storagePwd (base64 от 32 байт)
* - sigNum (0 или 1) * - timeMs (long, мс с 1970-01-01)
* - timeMs * - signatureB64 (подпись Ed25519 над строкой:
* - signatureB64 от строки (loginId + timeMs + sessionPwd) * "AUTHORIFICATED:" + timeMs + sessionPwd)
*
* Параметр sessionPwd клиент получил на шаге 1.
* Для проверки подписи используется pubkey1 (второй публичный ключ пользователя).
*
* Дополнительно:
* - timeMs должен отличаться от текущего времени сервера не более чем на 30 секунд.
*
* При успехе:
* - создаётся запись ActiveSession в БД;
* - генерируется sessionId (base64 от 32 случайных байт);
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
* - pushEndpoint / pushP256dhKey / pushAuthKey остаются пустыми;
* - возвращается sessionId в ответе.
*/ */
public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(NetAuthSessionNewStep2Handler.class); 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 @Override
public NetResponse handle(NetRequest baseReq, ConnectionContext ctx) throws Exception { public NetResponse handle(NetRequest baseReq, ConnectionContext ctx) throws Exception {
NetAuthSessionNewStep2Request req = (NetAuthSessionNewStep2Request) baseReq; NetAuthSessionNewStep2Request req = (NetAuthSessionNewStep2Request) baseReq;
@ -58,25 +74,23 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
} }
SolanaUser user = ctx.getSolanaUser(); SolanaUser user = ctx.getSolanaUser();
long reqLoginId = req.getLoginId(); Long loginId = user.getLoginId();
Long ctxLoginId = user.getLoginId(); if (loginId == null) {
if (ctxLoginId == null || ctxLoginId != reqLoginId) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.SERVER_DATA_ERROR,
"LOGIN_ID_MISMATCH", "NO_LOGIN_ID",
"loginId в запросе не совпадает с пользователем из шага 1" "Для пользователя не задан loginId в БД"
); );
} }
int sigNum = req.getSigNum(); String storagePwd = req.getStoragePwd();
if (sigNum != 0 && sigNum != 1) { if (storagePwd == null || storagePwd.isBlank()) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_SIG_NUM", "EMPTY_STORAGE_PWD",
"Номер подписи должен быть 0 или 1" "Пустой storagePwd"
); );
} }
@ -90,14 +104,28 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
); );
} }
// --- выбираем публичный ключ по sigNum --- long timeMs = req.getTimeMs();
String pubKeyB64 = (sigNum == 0) ? user.getPubkey0() : user.getPubkey1(); 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()) { if (pubKeyB64 == null || pubKeyB64.isBlank()) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"NO_PUBKEY", "NO_PUBKEY1",
"Отсутствует публичный ключ для выбранного номера подписи" "Отсутствует публичный ключ pubkey1 для пользователя"
); );
} }
@ -115,9 +143,8 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
); );
} }
// --- собираем строку для подписи: loginId + timeMs + sessionPwd --- // --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + sessionPwd ---
long timeMs = req.getTimeMs(); String preimageStr = "AUTHORIFICATED:" + timeMs + ctx.getSessionPwd();
String preimageStr = String.valueOf(reqLoginId) + timeMs + ctx.getSessionPwd();
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
@ -130,29 +157,30 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
); );
} }
// --- создаём уникальный sessionId и записываем в БД --- // --- создаём уникальный sessionId (base64 от 32 байт) и записываем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
long sessionId; String sessionId;
ActiveSession activeSession; ActiveSession activeSession;
try { try {
sessionId = generateUniqueSessionId(dao); sessionId = generateRandomSessionId();
long nowMs = System.currentTimeMillis(); long now = System.currentTimeMillis();
activeSession = new ActiveSession( activeSession = new ActiveSession(
sessionId, sessionId,
loginId,
ctx.getSessionPwd(), ctx.getSessionPwd(),
reqLoginId, storagePwd,
nowMs, now,
(short) sigNum, // pubkeyNum now,
null, // pushEndpoint null, // pushEndpoint
null, // pushP256dhKey null, // pushP256dhKey
null // pushAuthKey null // pushAuthKey
); );
dao.insert(activeSession); dao.insert(activeSession);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", reqLoginId, e); log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
@ -166,7 +194,6 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
ctx.setSessionId(sessionId); ctx.setSessionId(sessionId);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().removeBySessionId(sessionId); // га всякий случай предварительно удаляем что бы точно небыло дублирования активной сессии
// Регистрируем это подключение в глобальном реестре активных соединений // Регистрируем это подключение в глобальном реестре активных соединений
ActiveConnectionsRegistry.getInstance().register(ctx); ActiveConnectionsRegistry.getInstance().register(ctx);
@ -180,16 +207,11 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
} }
/** /**
* Генерация уникального sessionId с проверкой в БД. * Генерация случайного sessionId: base64-строка от 32 байт.
*/ */
private long generateUniqueSessionId(ActiveSessionsDAO dao) throws SQLException { private String generateRandomSessionId() {
for (int i = 0; i < 10; i++) { byte[] buf = new byte[32];
long candidate = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); RANDOM.nextBytes(buf);
ActiveSession existing = dao.getBySessionId(candidate); return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
if (existing == null) {
return candidate;
}
}
throw new SQLException("Не удалось сгенерировать уникальный sessionId за разумное число попыток");
} }
} }

View File

@ -20,6 +20,12 @@ import java.sql.SQLException;
/** /**
* Хэндлер SessionRefresh. * Хэндлер SessionRefresh.
*
* При успешной проверке sessionId + sessionPwd:
* - подтягивает пользователя по loginId из сессии;
* - заполняет ConnectionContext;
* - обновляет lastAuthirificatedAtMs в БД на текущее время;
* - возвращает storagePwd в payload.
*/ */
public class NetSessionRefreshHandler implements JsonMessageHandler { public class NetSessionRefreshHandler implements JsonMessageHandler {
@ -29,9 +35,18 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception { public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception {
NetSessionRefreshRequest req = (NetSessionRefreshRequest) request; NetSessionRefreshRequest req = (NetSessionRefreshRequest) request;
long sessionId = req.getSessionId(); String sessionId = req.getSessionId();
String sessionPwd = req.getSessionPwd(); 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()) { if (sessionPwd == null || sessionPwd.isEmpty()) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
@ -76,13 +91,10 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
// --- достаём пользователя по loginId из сессии --- // --- достаём пользователя по loginId из сессии ---
SolanaUser solanaUser = null; SolanaUser solanaUser = null;
Long loginId = null; long loginId = session.getLoginId();
try { try {
loginId = session.getLoginId(); SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
if (loginId != null) { solanaUser = usersDao.getByLoginId(loginId);
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
solanaUser = usersDao.getByLoginId(loginId);
}
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
@ -93,7 +105,7 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
); );
} }
if (loginId != null && solanaUser == null) { if (solanaUser == null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.UNVERIFIED,
@ -110,16 +122,24 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
ctx.setSessionPwd(sessionPwd); ctx.setSessionPwd(sessionPwd);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().removeBySessionId(sessionId); // на всякий случай удаляем что бы точно небыло повторов
// Регистрируем это подключение в глобальном реестре активных соединений // Регистрируем это подключение в глобальном реестре активных соединений
ActiveConnectionsRegistry.getInstance().register(ctx); 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(); NetSessionRefreshResponse resp = new NetSessionRefreshResponse();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
resp.setStoragePwd(session.getStoragePwd());
return resp; return resp;
} }
} }