09 12 25
В черновую переделал авторификацию
This commit is contained in:
parent
2b5fa16824
commit
2ed4f6d666
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user