В черновую переделал авторификацию
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.*;
/** Здесь мы хрним данные об активных сессиях пользователя (для 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

View File

@ -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;
}

View File

@ -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;
*/
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<Long, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
// sessionId (String) -> ConnectionContext
private final ConcurrentHashMap<String, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
// loginId -> множество ConnectionContext для этого пользователя
private final ConcurrentHashMap<Long, Set<ConnectionContext>> 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);
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,21 +157,22 @@ 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
storagePwd,
now,
now,
null, // pushEndpoint
null, // pushP256dhKey
null // pushAuthKey
@ -152,7 +180,7 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
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);
}
}

View File

@ -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);
}
} 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;
}
}