10 12 25
промежуточный не рабочий комит
This commit is contained in:
parent
95ec6ba037
commit
00fc9e3926
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -31,12 +31,13 @@ dependencies {
|
|||||||
|
|
||||||
|
|
||||||
implementation project(':shine-server-config') // модуль настроек из application.properties
|
implementation project(':shine-server-config') // модуль настроек из application.properties
|
||||||
implementation project('shine-server-geo') // модуль для определения геолокации по IP
|
|
||||||
|
|
||||||
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
|
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
|
||||||
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
|
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
|
||||||
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
||||||
|
|
||||||
|
implementation project(':shine-server-geo') // модуль для определения геолокации по IP
|
||||||
|
|
||||||
implementation project(':shine-server-net-protocol') // Модуль отвечающий за протокол (классы Net..Request/Response
|
implementation project(':shine-server-net-protocol') // Модуль отвечающий за протокол (классы Net..Request/Response
|
||||||
implementation project(':shine-server-net-server') // Хэндлеры для обработки сетевых запросов
|
implementation project(':shine-server-net-server') // Хэндлеры для обработки сетевых запросов
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,6 @@ public class DatabaseInitializer {
|
|||||||
|
|
||||||
Path dbFile = Paths.get(dbPath);
|
Path dbFile = Paths.get(dbPath);
|
||||||
try {
|
try {
|
||||||
// создаём директорию, если нужно
|
|
||||||
Path parent = dbFile.getParent();
|
Path parent = dbFile.getParent();
|
||||||
if (parent != null && !Files.exists(parent)) {
|
if (parent != null && !Files.exists(parent)) {
|
||||||
Files.createDirectories(parent);
|
Files.createDirectories(parent);
|
||||||
@ -75,18 +74,17 @@ public class DatabaseInitializer {
|
|||||||
try (Connection conn = DriverManager.getConnection(jdbcUrl);
|
try (Connection conn = DriverManager.getConnection(jdbcUrl);
|
||||||
Statement st = conn.createStatement()) {
|
Statement st = conn.createStatement()) {
|
||||||
|
|
||||||
// включаем внешние ключи на этом соединении (для инициализации тоже)
|
|
||||||
st.execute("PRAGMA foreign_keys = ON");
|
st.execute("PRAGMA foreign_keys = ON");
|
||||||
|
|
||||||
// 1. Таблица solana_users
|
// 1. solana_users
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS solana_users (
|
CREATE TABLE IF NOT EXISTS solana_users (
|
||||||
login TEXT NOT NULL,
|
login TEXT NOT NULL,
|
||||||
loginId INTEGER NOT NULL PRIMARY KEY,
|
loginId INTEGER NOT NULL PRIMARY KEY,
|
||||||
bchId INTEGER NOT NULL,
|
bchId INTEGER NOT NULL,
|
||||||
loginKey TEXT, -- основной публичный ключ (логин)
|
loginKey TEXT,
|
||||||
deviceKey TEXT, -- публичный ключ устройства
|
deviceKey TEXT,
|
||||||
bchLimit INTEGER -- может быть NULL
|
bchLimit INTEGER
|
||||||
);
|
);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
@ -95,8 +93,7 @@ public class DatabaseInitializer {
|
|||||||
ON solana_users (login);
|
ON solana_users (login);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 2. Таблица active_sessions
|
// 2. active_sessions
|
||||||
// sessionId TEXT (base64 от 32 байт).
|
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS active_sessions (
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
||||||
sessionId TEXT NOT NULL PRIMARY KEY,
|
sessionId TEXT NOT NULL PRIMARY KEY,
|
||||||
@ -108,6 +105,10 @@ public class DatabaseInitializer {
|
|||||||
pushEndpoint TEXT,
|
pushEndpoint TEXT,
|
||||||
pushP256dhKey TEXT,
|
pushP256dhKey TEXT,
|
||||||
pushAuthKey TEXT,
|
pushAuthKey TEXT,
|
||||||
|
clientIp TEXT,
|
||||||
|
clientInfoFromClient TEXT,
|
||||||
|
clientInfoFromRequest TEXT,
|
||||||
|
userLanguage TEXT,
|
||||||
FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
|
FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
|
||||||
);
|
);
|
||||||
""");
|
""");
|
||||||
@ -117,8 +118,7 @@ public class DatabaseInitializer {
|
|||||||
ON active_sessions (loginId);
|
ON active_sessions (loginId);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 3. Таблица users_params
|
// 3. users_params
|
||||||
// Пара (loginId, param) должна быть уникальна.
|
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS users_params (
|
CREATE TABLE IF NOT EXISTS users_params (
|
||||||
loginId INTEGER NOT NULL,
|
loginId INTEGER NOT NULL,
|
||||||
@ -138,7 +138,7 @@ public class DatabaseInitializer {
|
|||||||
ON users_params (loginId);
|
ON users_params (loginId);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 4. Таблица ip_geo_cache — персистентный кэш геолокации по IP
|
// 4. ip_geo_cache
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
||||||
ip TEXT NOT NULL PRIMARY KEY,
|
ip TEXT NOT NULL PRIMARY KEY,
|
||||||
|
|||||||
@ -9,6 +9,25 @@ import java.sql.*;
|
|||||||
* DAO для таблицы active_sessions.
|
* DAO для таблицы active_sessions.
|
||||||
*
|
*
|
||||||
* Здесь мы храним данные об активных сессиях пользователя (для wss-соединений).
|
* Здесь мы храним данные об активных сессиях пользователя (для wss-соединений).
|
||||||
|
*
|
||||||
|
* Структура таблицы:
|
||||||
|
*
|
||||||
|
* CREATE TABLE active_sessions (
|
||||||
|
* sessionId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
* loginId INTEGER NOT NULL,
|
||||||
|
* sessionPwd TEXT NOT NULL,
|
||||||
|
* storagePwd TEXT NOT NULL,
|
||||||
|
* sessionCreatedAtMs INTEGER NOT NULL,
|
||||||
|
* lastAuthirificatedAtMs INTEGER NOT NULL,
|
||||||
|
* pushEndpoint TEXT,
|
||||||
|
* pushP256dhKey TEXT,
|
||||||
|
* pushAuthKey TEXT,
|
||||||
|
* clientIp TEXT,
|
||||||
|
* clientInfoFromClient TEXT,
|
||||||
|
* clientInfoFromRequest TEXT,
|
||||||
|
* userLanguage TEXT,
|
||||||
|
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
|
||||||
|
* );
|
||||||
*/
|
*/
|
||||||
public final class ActiveSessionsDAO {
|
public final class ActiveSessionsDAO {
|
||||||
|
|
||||||
@ -43,8 +62,12 @@ public final class ActiveSessionsDAO {
|
|||||||
lastAuthirificatedAtMs,
|
lastAuthirificatedAtMs,
|
||||||
pushEndpoint,
|
pushEndpoint,
|
||||||
pushP256dhKey,
|
pushP256dhKey,
|
||||||
pushAuthKey
|
pushAuthKey,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
||||||
@ -57,6 +80,10 @@ public final class ActiveSessionsDAO {
|
|||||||
ps.setString(7, session.getPushEndpoint());
|
ps.setString(7, session.getPushEndpoint());
|
||||||
ps.setString(8, session.getPushP256dhKey());
|
ps.setString(8, session.getPushP256dhKey());
|
||||||
ps.setString(9, session.getPushAuthKey());
|
ps.setString(9, session.getPushAuthKey());
|
||||||
|
ps.setString(10, session.getClientIp());
|
||||||
|
ps.setString(11, session.getClientInfoFromClient());
|
||||||
|
ps.setString(12, session.getClientInfoFromRequest());
|
||||||
|
ps.setString(13, session.getUserLanguage());
|
||||||
|
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -76,7 +103,11 @@ public final class ActiveSessionsDAO {
|
|||||||
lastAuthirificatedAtMs,
|
lastAuthirificatedAtMs,
|
||||||
pushEndpoint,
|
pushEndpoint,
|
||||||
pushP256dhKey,
|
pushP256dhKey,
|
||||||
pushAuthKey
|
pushAuthKey,
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
FROM active_sessions
|
FROM active_sessions
|
||||||
WHERE sessionId = ?
|
WHERE sessionId = ?
|
||||||
""";
|
""";
|
||||||
@ -94,6 +125,7 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
||||||
|
* (оставляю для совместимости, вдруг ещё где-то используется)
|
||||||
*/
|
*/
|
||||||
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
@ -109,6 +141,45 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление метаданных при RefreshSession:
|
||||||
|
* - lastAuthirificatedAtMs
|
||||||
|
* - clientIp
|
||||||
|
* - clientInfoFromClient
|
||||||
|
* - clientInfoFromRequest
|
||||||
|
* - userLanguage
|
||||||
|
*/
|
||||||
|
public void updateOnRefresh(
|
||||||
|
String sessionId,
|
||||||
|
long lastAuthMs,
|
||||||
|
String clientIp,
|
||||||
|
String clientInfoFromClient,
|
||||||
|
String clientInfoFromRequest,
|
||||||
|
String userLanguage
|
||||||
|
) throws SQLException {
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
UPDATE active_sessions
|
||||||
|
SET
|
||||||
|
lastAuthirificatedAtMs = ?,
|
||||||
|
clientIp = ?,
|
||||||
|
clientInfoFromClient = ?,
|
||||||
|
clientInfoFromRequest = ?,
|
||||||
|
userLanguage = ?
|
||||||
|
WHERE sessionId = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, lastAuthMs);
|
||||||
|
ps.setString(2, clientIp);
|
||||||
|
ps.setString(3, clientInfoFromClient);
|
||||||
|
ps.setString(4, clientInfoFromRequest);
|
||||||
|
ps.setString(5, userLanguage);
|
||||||
|
ps.setString(6, sessionId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удаление записи по sessionId.
|
* Удаление записи по sessionId.
|
||||||
* Если записи нет — просто ничего не удалит (0 строк).
|
* Если записи нет — просто ничего не удалит (0 строк).
|
||||||
@ -122,6 +193,9 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Маппинг ResultSet → ActiveSession (все 13 полей).
|
||||||
|
*/
|
||||||
private ActiveSession mapRow(ResultSet rs) throws SQLException {
|
private ActiveSession mapRow(ResultSet rs) throws SQLException {
|
||||||
String sessionId = rs.getString("sessionId");
|
String sessionId = rs.getString("sessionId");
|
||||||
long loginId = rs.getLong("loginId");
|
long loginId = rs.getLong("loginId");
|
||||||
@ -132,6 +206,10 @@ public final class ActiveSessionsDAO {
|
|||||||
String pushEndpoint = rs.getString("pushEndpoint");
|
String pushEndpoint = rs.getString("pushEndpoint");
|
||||||
String pushP256dhKey = rs.getString("pushP256dhKey");
|
String pushP256dhKey = rs.getString("pushP256dhKey");
|
||||||
String pushAuthKey = rs.getString("pushAuthKey");
|
String pushAuthKey = rs.getString("pushAuthKey");
|
||||||
|
String clientIp = rs.getString("clientIp");
|
||||||
|
String clientInfoFromClient = rs.getString("clientInfoFromClient");
|
||||||
|
String clientInfoFromRequest = rs.getString("clientInfoFromRequest");
|
||||||
|
String userLanguage = rs.getString("userLanguage");
|
||||||
|
|
||||||
return new ActiveSession(
|
return new ActiveSession(
|
||||||
sessionId,
|
sessionId,
|
||||||
@ -142,7 +220,11 @@ public final class ActiveSessionsDAO {
|
|||||||
lastAuthirificatedAtMs,
|
lastAuthirificatedAtMs,
|
||||||
pushEndpoint,
|
pushEndpoint,
|
||||||
pushP256dhKey,
|
pushP256dhKey,
|
||||||
pushAuthKey
|
pushAuthKey,
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,8 +3,6 @@ package shine.db.entities;
|
|||||||
/**
|
/**
|
||||||
* Модель активной сессии (таблица active_sessions).
|
* Модель активной сессии (таблица active_sessions).
|
||||||
*
|
*
|
||||||
* Поля соответствуют схеме:
|
|
||||||
*
|
|
||||||
* CREATE TABLE active_sessions (
|
* CREATE TABLE active_sessions (
|
||||||
* sessionId TEXT NOT NULL PRIMARY KEY,
|
* sessionId TEXT NOT NULL PRIMARY KEY,
|
||||||
* loginId INTEGER NOT NULL,
|
* loginId INTEGER NOT NULL,
|
||||||
@ -15,6 +13,10 @@ package shine.db.entities;
|
|||||||
* pushEndpoint TEXT,
|
* pushEndpoint TEXT,
|
||||||
* pushP256dhKey TEXT,
|
* pushP256dhKey TEXT,
|
||||||
* pushAuthKey TEXT,
|
* pushAuthKey TEXT,
|
||||||
|
* clientIp TEXT,
|
||||||
|
* clientInfoFromClient TEXT,
|
||||||
|
* clientInfoFromRequest TEXT,
|
||||||
|
* userLanguage TEXT,
|
||||||
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
|
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
|
||||||
* );
|
* );
|
||||||
*/
|
*/
|
||||||
@ -30,6 +32,12 @@ public class ActiveSession {
|
|||||||
private String pushP256dhKey; // TEXT (nullable)
|
private String pushP256dhKey; // TEXT (nullable)
|
||||||
private String pushAuthKey; // TEXT (nullable)
|
private String pushAuthKey; // TEXT (nullable)
|
||||||
|
|
||||||
|
// Новые поля
|
||||||
|
private String clientIp; // IP клиента при auth/refresh
|
||||||
|
private String clientInfoFromClient; // строка от клиента (PWA)
|
||||||
|
private String clientInfoFromRequest; // строка, собранная на сервере
|
||||||
|
private String userLanguage; // prefer-language (например, "ru-RU")
|
||||||
|
|
||||||
public ActiveSession() {
|
public ActiveSession() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +49,11 @@ public class ActiveSession {
|
|||||||
long lastAuthirificatedAtMs,
|
long lastAuthirificatedAtMs,
|
||||||
String pushEndpoint,
|
String pushEndpoint,
|
||||||
String pushP256dhKey,
|
String pushP256dhKey,
|
||||||
String pushAuthKey) {
|
String pushAuthKey,
|
||||||
|
String clientIp,
|
||||||
|
String clientInfoFromClient,
|
||||||
|
String clientInfoFromRequest,
|
||||||
|
String userLanguage) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.loginId = loginId;
|
this.loginId = loginId;
|
||||||
this.sessionPwd = sessionPwd;
|
this.sessionPwd = sessionPwd;
|
||||||
@ -51,6 +63,10 @@ public class ActiveSession {
|
|||||||
this.pushEndpoint = pushEndpoint;
|
this.pushEndpoint = pushEndpoint;
|
||||||
this.pushP256dhKey = pushP256dhKey;
|
this.pushP256dhKey = pushP256dhKey;
|
||||||
this.pushAuthKey = pushAuthKey;
|
this.pushAuthKey = pushAuthKey;
|
||||||
|
this.clientIp = clientIp;
|
||||||
|
this.clientInfoFromClient = clientInfoFromClient;
|
||||||
|
this.clientInfoFromRequest = clientInfoFromRequest;
|
||||||
|
this.userLanguage = userLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- getters / setters ---
|
// --- getters / setters ---
|
||||||
@ -126,4 +142,36 @@ public class ActiveSession {
|
|||||||
public void setPushAuthKey(String pushAuthKey) {
|
public void setPushAuthKey(String pushAuthKey) {
|
||||||
this.pushAuthKey = pushAuthKey;
|
this.pushAuthKey = pushAuthKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientIp() {
|
||||||
|
return clientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientIp(String clientIp) {
|
||||||
|
this.clientIp = clientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientInfoFromClient() {
|
||||||
|
return clientInfoFromClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfoFromClient(String clientInfoFromClient) {
|
||||||
|
this.clientInfoFromClient = clientInfoFromClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientInfoFromRequest() {
|
||||||
|
return clientInfoFromRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfoFromRequest(String clientInfoFromRequest) {
|
||||||
|
this.clientInfoFromRequest = clientInfoFromRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserLanguage() {
|
||||||
|
return userLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserLanguage(String userLanguage) {
|
||||||
|
this.userLanguage = userLanguage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -22,6 +22,9 @@ dependencies {
|
|||||||
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
|
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
|
||||||
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
|
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
|
||||||
|
|
||||||
|
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|||||||
@ -2,18 +2,25 @@ package shine.geo;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import shine.db.dao.IpGeoCacheDAO;
|
||||||
|
import shine.db.entities.IpGeoCacheEntry;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис для геолокации по IP.
|
* Сервис для геолокации по IP.
|
||||||
* .
|
*
|
||||||
* Основной метод:
|
* Основной метод без кэша:
|
||||||
* resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN, если не удалось.
|
* resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN
|
||||||
|
*
|
||||||
|
* Метод с кэшированием в БД:
|
||||||
|
* resolveCountryCityOrIpWithCache(ip) -> сначала смотрит в ip_geo_cache,
|
||||||
|
* при отсутствии записи — обращается к внешнему сервису, сохраняет результат в кэш и возвращает его.
|
||||||
*/
|
*/
|
||||||
public final class GeoLookupService {
|
public final class GeoLookupService {
|
||||||
|
|
||||||
@ -34,6 +41,8 @@ public final class GeoLookupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* ВАРИАНТ БЕЗ КЭША.
|
||||||
|
*
|
||||||
* Возвращает строку вида "Country, City" по IP.
|
* Возвращает строку вида "Country, City" по IP.
|
||||||
* Если запрос не удался, возвращает GEO_UNKNOWN.
|
* Если запрос не удался, возвращает GEO_UNKNOWN.
|
||||||
*/
|
*/
|
||||||
@ -79,7 +88,6 @@ public final class GeoLookupService {
|
|||||||
return GEO_UNKNOWN;
|
return GEO_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Собираем строку
|
|
||||||
if (country != null && city != null) {
|
if (country != null && city != null) {
|
||||||
return country + ", " + city;
|
return country + ", " + city;
|
||||||
} else if (country != null) {
|
} else if (country != null) {
|
||||||
@ -94,6 +102,64 @@ public final class GeoLookupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ВАРИАНТ С КЭШЕМ В БАЗЕ (ip_geo_cache).
|
||||||
|
*
|
||||||
|
* Логика:
|
||||||
|
* 1) Если IP пустой или локальный — сразу GEO_UNKNOWN (и ничего не пишем в кэш).
|
||||||
|
* 2) Пытаемся найти ip в ip_geo_cache:
|
||||||
|
* - если нашли — возвращаем geo из записи.
|
||||||
|
* 3) Если не нашли — вызываем resolveCountryCityOrIp(ip) (внешний сервис),
|
||||||
|
* - результат (включая GEO_UNKNOWN) сохраняем в ip_geo_cache через IpGeoCacheDAO.upsert()
|
||||||
|
* - возвращаем сохранённый результат.
|
||||||
|
*
|
||||||
|
* В случае ошибок БД — просто падаем назад на поведение без кэша.
|
||||||
|
*/
|
||||||
|
public static String resolveCountryCityOrIpWithCache(String ip) {
|
||||||
|
if (ip == null || ip.isBlank()) {
|
||||||
|
return GEO_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приватные/локальные IP не кешируем и не запрашиваем
|
||||||
|
if (isPrivateOrLocalIp(ip)) {
|
||||||
|
return GEO_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Сначала пробуем взять из кэша
|
||||||
|
IpGeoCacheDAO dao = IpGeoCacheDAO.getInstance();
|
||||||
|
try {
|
||||||
|
IpGeoCacheEntry cached = dao.getByIp(ip);
|
||||||
|
if (cached != null) {
|
||||||
|
String geo = cached.getGeo();
|
||||||
|
if (geo != null && !geo.isBlank()) {
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
// Если geo пустая строка (на всякий случай) — идём за свежими данными.
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// Ошибка БД — логируем при желании и продолжаем без кэша
|
||||||
|
// log.warn("Failed to read IP geo cache", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Вызываем "сырой" метод, который ходит во внешний сервис
|
||||||
|
String resolvedGeo = resolveCountryCityOrIp(ip);
|
||||||
|
|
||||||
|
// 3. Пишем результат в кэш (включая GEO_UNKNOWN)
|
||||||
|
try {
|
||||||
|
IpGeoCacheEntry entry = new IpGeoCacheEntry(
|
||||||
|
ip,
|
||||||
|
resolvedGeo,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
dao.upsert(entry);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// Ошибка БД при записи — просто игнорируем, кэш не обязателен для работы
|
||||||
|
// log.warn("Failed to upsert IP geo cache", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedGeo;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Пытается получить внешний IP текущей машины через HTTP-сервис.
|
* Пытается получить внешний IP текущей машины через HTTP-сервис.
|
||||||
* В случае ошибки возвращает fallbackIp.
|
* В случае ошибки возвращает fallbackIp.
|
||||||
|
|||||||
@ -29,6 +29,8 @@ dependencies {
|
|||||||
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
|
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
|
||||||
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
|
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
|
||||||
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
||||||
|
implementation project(':shine-server-geo') // модуль для определения геолокации по IP
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
|||||||
/**
|
/**
|
||||||
* Ответ на AuthChallenge.
|
* Ответ на AuthChallenge.
|
||||||
*
|
*
|
||||||
* При успехе сервер возвращает временный секрет sessionPwd,
|
* При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
|
||||||
* который клиент обязан использовать на втором шаге при формировании подписи.
|
* который клиент обязан использовать на втором шаге при формировании строки
|
||||||
|
* для цифровой подписи.
|
||||||
*
|
*
|
||||||
* JSON:
|
* JSON:
|
||||||
* {
|
* {
|
||||||
@ -14,22 +15,23 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
|||||||
* "requestId": "...",
|
* "requestId": "...",
|
||||||
* "status": 200,
|
* "status": 200,
|
||||||
* "payload": {
|
* "payload": {
|
||||||
* "sessionPwd": "base64-строка-от-32-байт"
|
* "authNonce": "base64-строка-от-32-байт"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public class Net_AuthChallenge_Response extends NetResponse {
|
public class Net_AuthChallenge_Response extends NetResponse {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Временный секрет, сгенерированный сервером.
|
* Одноразовый nonce для авторификации.
|
||||||
* Строка — это base64-представление 32 случайных байт.
|
* Строка — это base64-представление 32 случайных байт.
|
||||||
*/
|
*/
|
||||||
private String sessionPwd;
|
private String authNonce;
|
||||||
|
|
||||||
public String getSessionPwd() {
|
public String getAuthNonce() {
|
||||||
return sessionPwd;
|
return authNonce;
|
||||||
}
|
}
|
||||||
public void setSessionPwd(String sessionPwd) {
|
|
||||||
this.sessionPwd = sessionPwd;
|
public void setAuthNonce(String authNonce) {
|
||||||
|
this.authNonce = authNonce;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,13 +6,16 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
|||||||
* Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
|
* Шаг 2 авторизации: подтверждение владения ключом и установка сессии.
|
||||||
*
|
*
|
||||||
* Клиент:
|
* Клиент:
|
||||||
* 1) получает от сервера sessionPwd на шаге 1;
|
* 1) получает от сервера authNonce на шаге 1;
|
||||||
* 2) генерирует свой StoragePwd (base64 от 32 байт);
|
* 2) генерирует свой StoragePwd (base64 от 32 байт);
|
||||||
* 3) формирует строку для подписи:
|
* 3) формирует строку для подписи:
|
||||||
* "AUTHORIFICATED:" + timeMs + sessionPwd
|
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
* 4) подписывает эту строку своим приватным ключом (pubkey1),
|
* 4) подписывает эту строку своим приватным ключом (pubkey1),
|
||||||
* отправляет подпись и StoragePwd на сервер.
|
* отправляет подпись и StoragePwd на сервер.
|
||||||
*
|
*
|
||||||
|
* Дополнительно:
|
||||||
|
* - clientInfo — короткая строка (до 50 символов) с данными об устройстве/клиенте.
|
||||||
|
*
|
||||||
* Формат входящего JSON:
|
* Формат входящего JSON:
|
||||||
* {
|
* {
|
||||||
* "op": "CreateAuthSession",
|
* "op": "CreateAuthSession",
|
||||||
@ -20,12 +23,10 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
|||||||
* "payload": {
|
* "payload": {
|
||||||
* "storagePwd": "base64-строка-от-32-байт",
|
* "storagePwd": "base64-строка-от-32-байт",
|
||||||
* "timeMs": 1733310000000,
|
* "timeMs": 1733310000000,
|
||||||
* "signatureB64": "base64-подпись-Ed25519"
|
* "signatureB64": "base64-подпись-Ed25519",
|
||||||
|
* "clientInfo": "Chrome/Android" // опционально, до 50 символов
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
|
||||||
* При успешной проверке подписи сервер создаёт запись в active_sessions
|
|
||||||
* и возвращает sessionId (base64-строка от 32 байт).
|
|
||||||
*/
|
*/
|
||||||
public class Net_CreateAuthSession_Request extends NetRequest {
|
public class Net_CreateAuthSession_Request extends NetRequest {
|
||||||
|
|
||||||
@ -35,9 +36,12 @@ public class Net_CreateAuthSession_Request extends NetRequest {
|
|||||||
/** Время на стороне клиента (мс с 1970-01-01). */
|
/** Время на стороне клиента (мс с 1970-01-01). */
|
||||||
private long timeMs;
|
private long timeMs;
|
||||||
|
|
||||||
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + sessionPwd (base64). */
|
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||||
private String signatureB64;
|
private String signatureB64;
|
||||||
|
|
||||||
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
|
private String clientInfo;
|
||||||
|
|
||||||
public String getStoragePwd() {
|
public String getStoragePwd() {
|
||||||
return storagePwd;
|
return storagePwd;
|
||||||
}
|
}
|
||||||
@ -61,4 +65,12 @@ public class Net_CreateAuthSession_Request extends NetRequest {
|
|||||||
public void setSignatureB64(String signatureB64) {
|
public void setSignatureB64(String signatureB64) {
|
||||||
this.signatureB64 = signatureB64;
|
this.signatureB64 = signatureB64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientInfo() {
|
||||||
|
return clientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfo(String clientInfo) {
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
|||||||
* Ответ на CreateAuthSession.
|
* Ответ на CreateAuthSession.
|
||||||
*
|
*
|
||||||
* При успехе сервер создаёт запись в active_sessions
|
* При успехе сервер создаёт запись в active_sessions
|
||||||
* и возвращает идентификатор сессии sessionId.
|
* и возвращает идентификатор сессии sessionId и секрет сессии sessionPwd.
|
||||||
*
|
*
|
||||||
* JSON:
|
* JSON:
|
||||||
* {
|
* {
|
||||||
@ -14,7 +14,8 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
|
|||||||
* "requestId": "...",
|
* "requestId": "...",
|
||||||
* "status": 200,
|
* "status": 200,
|
||||||
* "payload": {
|
* "payload": {
|
||||||
* "sessionId": "base64-строка-от-32-байт"
|
* "sessionId": "base64-строка-от-32-байт",
|
||||||
|
* "sessionPwd": "base64-строка-от-32-байт"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
@ -23,6 +24,9 @@ public class Net_CreateAuthSession_Response extends NetResponse {
|
|||||||
/** Идентификатор сессии, base64 от 32 байт. */
|
/** Идентификатор сессии, base64 от 32 байт. */
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
|
/** Секрет сессии, base64 от 32 байт. */
|
||||||
|
private String sessionPwd;
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@ -30,4 +34,12 @@ public class Net_CreateAuthSession_Response extends NetResponse {
|
|||||||
public void setSessionId(String sessionId) {
|
public void setSessionId(String sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSessionPwd() {
|
||||||
|
return sessionPwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionPwd(String sessionPwd) {
|
||||||
|
this.sessionPwd = sessionPwd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -11,7 +11,8 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
|
|||||||
* JSON (payload):
|
* JSON (payload):
|
||||||
* {
|
* {
|
||||||
* "sessionId": "base64-id-сессии",
|
* "sessionId": "base64-id-сессии",
|
||||||
* "sessionPwd": "base64-sessionPwd"
|
* "sessionPwd": "base64-sessionPwd",
|
||||||
|
* "clientInfo": "до 50 символов, краткая строка об устройстве"
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public class Net_RefreshSession_Request extends NetRequest {
|
public class Net_RefreshSession_Request extends NetRequest {
|
||||||
@ -19,6 +20,12 @@ public class Net_RefreshSession_Request extends NetRequest {
|
|||||||
private String sessionId;
|
private String sessionId;
|
||||||
private String sessionPwd;
|
private String sessionPwd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Краткая строка с информацией об устройстве/клиенте, до 50 символов.
|
||||||
|
* Например: "PWA/Chrome/Android".
|
||||||
|
*/
|
||||||
|
private String clientInfo;
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@ -34,4 +41,12 @@ public class Net_RefreshSession_Request extends NetRequest {
|
|||||||
public void setSessionPwd(String sessionPwd) {
|
public void setSessionPwd(String sessionPwd) {
|
||||||
this.sessionPwd = sessionPwd;
|
this.sessionPwd = sessionPwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientInfo() {
|
||||||
|
return clientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfo(String clientInfo) {
|
||||||
|
this.clientInfo = clientInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -46,7 +46,6 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login);
|
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||||
|
|
||||||
if (solanaUser == null) {
|
if (solanaUser == null) {
|
||||||
// TODO позже — запрос в Solana, если не нашли локально
|
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
@ -55,22 +54,23 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Заполняем контекст целиком пользователем
|
// 3) Заполняем контекст пользователем
|
||||||
ctx.setSolanaUser(solanaUser);
|
ctx.setSolanaUser(solanaUser);
|
||||||
|
|
||||||
// 4) Генерируем надёжный sessionPwd = base64(32 случайных байт)
|
// 4) Генерируем одноразовый authNonce = base64(32 случайных байт)
|
||||||
byte[] buf = new byte[32];
|
byte[] buf = new byte[32];
|
||||||
RANDOM.nextBytes(buf);
|
RANDOM.nextBytes(buf);
|
||||||
String sessionPwd = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
|
|
||||||
ctx.setSessionPwd(sessionPwd);
|
// Используем поле sessionPwd в контексте как хранилище challenge (authNonce) до шага 2
|
||||||
|
ctx.setSessionPwd(authNonce);
|
||||||
|
|
||||||
// 5) Формируем ответ
|
// 5) Формируем ответ
|
||||||
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
|
Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
|
||||||
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);
|
resp.setAuthNonce(authNonce);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,13 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.entities.ActiveSession;
|
import shine.db.entities.ActiveSession;
|
||||||
import shine.db.entities.SolanaUser;
|
import shine.db.entities.SolanaUser;
|
||||||
|
import shine.geo.ClientInfoService;
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -28,20 +33,18 @@ import java.util.Base64;
|
|||||||
* - storagePwd (base64 от 32 байт)
|
* - storagePwd (base64 от 32 байт)
|
||||||
* - timeMs (long, мс с 1970-01-01)
|
* - timeMs (long, мс с 1970-01-01)
|
||||||
* - signatureB64 (подпись Ed25519 над строкой:
|
* - signatureB64 (подпись Ed25519 над строкой:
|
||||||
* "AUTHORIFICATED:" + timeMs + sessionPwd)
|
* "AUTHORIFICATED:" + timeMs + authNonce)
|
||||||
|
* - clientInfo (опционально, до 50 символов)
|
||||||
*
|
*
|
||||||
* Параметр sessionPwd клиент получил на шаге 1.
|
* authNonce клиент получил на шаге 1 (AuthChallenge).
|
||||||
* Для проверки подписи используется pubkey1 (второй публичный ключ пользователя).
|
|
||||||
*
|
|
||||||
* Дополнительно:
|
|
||||||
* - timeMs должен отличаться от текущего времени сервера не более чем на 30 секунд.
|
|
||||||
*
|
*
|
||||||
* При успехе:
|
* При успехе:
|
||||||
* - создаётся запись ActiveSession в БД;
|
* - создаётся запись ActiveSession в БД;
|
||||||
* - генерируется sessionId (base64 от 32 случайных байт);
|
* - генерируется sessionId (base64 от 32 случайных байт);
|
||||||
|
* - генерируется sessionPwd (base64 от 32 случайных байт);
|
||||||
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
|
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
|
||||||
* - pushEndpoint / pushP256dhKey / pushAuthKey остаются пустыми;
|
* - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage;
|
||||||
* - возвращается sessionId в ответе.
|
* - возвращается sessionId и sessionPwd в ответе.
|
||||||
*/
|
*/
|
||||||
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@ -107,7 +110,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
long timeMs = req.getTimeMs();
|
long timeMs = req.getTimeMs();
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
|
|
||||||
// Проверка, что время клиента не отличается от времени сервера больше чем на 30 секунд
|
|
||||||
long diff = Math.abs(nowMs - timeMs);
|
long diff = Math.abs(nowMs - timeMs);
|
||||||
if (diff > ALLOWED_SKEW_MS) {
|
if (diff > ALLOWED_SKEW_MS) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
@ -118,6 +120,12 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Короткая строка clientInfo от клиента (до 50 символов)
|
||||||
|
String clientInfoFromClient = req.getClientInfo();
|
||||||
|
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
|
||||||
|
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
// --- выбираем публичный ключ pubkey1 ---
|
// --- выбираем публичный ключ pubkey1 ---
|
||||||
String pubKeyB64 = user.getDeviceKey();
|
String pubKeyB64 = user.getDeviceKey();
|
||||||
if (pubKeyB64 == null || pubKeyB64.isBlank()) {
|
if (pubKeyB64 == null || pubKeyB64.isBlank()) {
|
||||||
@ -143,8 +151,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + sessionPwd ---
|
// --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 ---
|
||||||
String preimageStr = "AUTHORIFICATED:" + timeMs + ctx.getSessionPwd();
|
String authNonce = ctx.getSessionPwd();
|
||||||
|
|
||||||
|
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + authNonce ---
|
||||||
|
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||||||
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);
|
||||||
@ -157,25 +168,47 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- создаём уникальный sessionId (base64 от 32 байт) и записываем в БД ---
|
// --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId ---
|
||||||
|
String newSessionPwd = generateRandomSecret();
|
||||||
|
String sessionId = generateRandomSessionId();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// --- Сбор данных о клиенте (IP, UA, язык) ---
|
||||||
|
Session wsSession = ctx.getWsSession();
|
||||||
|
String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
|
||||||
|
String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
|
||||||
|
|
||||||
|
String clientIp = "";
|
||||||
|
if (wsSession != null) {
|
||||||
|
SocketAddress rawAddr = wsSession.getRemoteAddress();
|
||||||
|
if (rawAddr instanceof InetSocketAddress inet) {
|
||||||
|
if (inet.getAddress() != null) {
|
||||||
|
clientIp = inet.getAddress().getHostAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO и сдесь тоже переписываем получение ИП адреса на стандартный метод и тоже дёргаем запрос геолокации который никуда не сохраняем просто что бы он в кэш сервера попал
|
||||||
|
|
||||||
|
|
||||||
|
// --- создаём запись ActiveSession и сохраняем в БД ---
|
||||||
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
|
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
|
||||||
String sessionId;
|
|
||||||
ActiveSession activeSession;
|
ActiveSession activeSession;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sessionId = generateRandomSessionId();
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
|
|
||||||
activeSession = new ActiveSession(
|
activeSession = new ActiveSession(
|
||||||
sessionId,
|
sessionId,
|
||||||
loginId,
|
loginId,
|
||||||
ctx.getSessionPwd(),
|
newSessionPwd, // настоящий секрет сессии
|
||||||
storagePwd,
|
storagePwd,
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
null, // pushEndpoint
|
null, // pushEndpoint
|
||||||
null, // pushP256dhKey
|
null, // pushP256dhKey
|
||||||
null // pushAuthKey
|
null, // pushAuthKey
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
);
|
);
|
||||||
|
|
||||||
dao.insert(activeSession);
|
dao.insert(activeSession);
|
||||||
@ -192,9 +225,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
// --- обновляем контекст ---
|
// --- обновляем контекст ---
|
||||||
ctx.setActiveSession(activeSession);
|
ctx.setActiveSession(activeSession);
|
||||||
ctx.setSessionId(sessionId);
|
ctx.setSessionId(sessionId);
|
||||||
|
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии, а не authNonce
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
// Регистрируем это подключение в глобальном реестре активных соединений
|
|
||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
|
|
||||||
// --- формируем ответ ---
|
// --- формируем ответ ---
|
||||||
@ -202,7 +235,8 @@ public class Net_CreateAuthSession__Handler 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.setSessionId(sessionId); // попадёт в payload.sessionId
|
resp.setSessionId(sessionId);
|
||||||
|
resp.setSessionPwd(newSessionPwd);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,4 +248,13 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
RANDOM.nextBytes(buf);
|
RANDOM.nextBytes(buf);
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерация случайного секрета (sessionPwd): base64-строка от 32 байт.
|
||||||
|
*/
|
||||||
|
private String generateRandomSecret() {
|
||||||
|
byte[] buf = new byte[32];
|
||||||
|
RANDOM.nextBytes(buf);
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -15,6 +15,7 @@ import shine.db.dao.ActiveSessionsDAO;
|
|||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
import shine.db.entities.ActiveSession;
|
import shine.db.entities.ActiveSession;
|
||||||
import shine.db.entities.SolanaUser;
|
import shine.db.entities.SolanaUser;
|
||||||
|
import shine.geo.ClientInfoService;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
@ -24,19 +25,23 @@ import java.sql.SQLException;
|
|||||||
* При успешной проверке sessionId + sessionPwd:
|
* При успешной проверке sessionId + sessionPwd:
|
||||||
* - подтягивает пользователя по loginId из сессии;
|
* - подтягивает пользователя по loginId из сессии;
|
||||||
* - заполняет ConnectionContext;
|
* - заполняет ConnectionContext;
|
||||||
* - обновляет lastAuthirificatedAtMs в БД на текущее время;
|
* - обновляет lastAuthirificatedAtMs и метаданные сессии в БД;
|
||||||
* - возвращает storagePwd в payload.
|
* - возвращает storagePwd в payload.
|
||||||
*/
|
*/
|
||||||
public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_RefreshSession_Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_RefreshSession_Handler.class);
|
||||||
|
|
||||||
|
// максимум 50 символов для clientInfo от клиента
|
||||||
|
private static final int CLIENT_INFO_MAX_LEN = 50;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception {
|
public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception {
|
||||||
Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
Net_RefreshSession_Request req = (Net_RefreshSession_Request) request;
|
||||||
|
|
||||||
String sessionId = req.getSessionId();
|
String sessionId = req.getSessionId();
|
||||||
String sessionPwd = req.getSessionPwd();
|
String sessionPwd = req.getSessionPwd();
|
||||||
|
String clientInfoFromClient = trimClientInfo(req.getClientInfo());
|
||||||
|
|
||||||
if (sessionId == null || sessionId.isBlank()) {
|
if (sessionId == null || sessionId.isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
@ -89,7 +94,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- достаём пользователя по loginId из сессии ---
|
// --- вытаскиваем пользователя по loginId ---
|
||||||
SolanaUser solanaUser = null;
|
SolanaUser solanaUser = null;
|
||||||
long loginId = session.getLoginId();
|
long loginId = session.getLoginId();
|
||||||
try {
|
try {
|
||||||
@ -114,7 +119,43 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Всё хорошо — обновляем контекст соединения
|
// --- собираем данные о клиенте из WebSocket-запроса ---
|
||||||
|
String clientIp = null;
|
||||||
|
String clientInfoFromRequest = null;
|
||||||
|
String userLanguage = null;
|
||||||
|
|
||||||
|
if (ctx != null && ctx.getWsSession() != null) {
|
||||||
|
clientIp = "8.8.8.8"; //TODO сделать нормальное получение ип адреса
|
||||||
|
// и сделать запрос геолокации и никуда его не сохранять запрос нужен просто что бы в кэш данные добавилиь если нужно
|
||||||
|
clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
|
||||||
|
userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// --- обновляем запись в БД (lastAuth + мета) ---
|
||||||
|
try {
|
||||||
|
sessionsDao.updateOnRefresh(
|
||||||
|
sessionId,
|
||||||
|
nowMs,
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
|
);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Ошибка БД при обновлении метаданных сессии sessionId={}", sessionId, e);
|
||||||
|
// не роняем авторизацию, но логируем
|
||||||
|
}
|
||||||
|
|
||||||
|
// Также обновим объект session в памяти (если дальше кто-то его использует)
|
||||||
|
session.setLastAuthirificatedAtMs(nowMs);
|
||||||
|
session.setClientIp(clientIp);
|
||||||
|
session.setClientInfoFromClient(clientInfoFromClient);
|
||||||
|
session.setClientInfoFromRequest(clientInfoFromRequest);
|
||||||
|
session.setUserLanguage(userLanguage);
|
||||||
|
|
||||||
|
// --- обновляем контекст соединения ---
|
||||||
if (ctx != null) {
|
if (ctx != null) {
|
||||||
ctx.setActiveSession(session);
|
ctx.setActiveSession(session);
|
||||||
ctx.setSolanaUser(solanaUser);
|
ctx.setSolanaUser(solanaUser);
|
||||||
@ -126,15 +167,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
|||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем lastAuthirificatedAtMs в БД
|
// --- ответ OK + storagePwd ---
|
||||||
try {
|
|
||||||
long nowMs = System.currentTimeMillis();
|
|
||||||
sessionsDao.updateLastAuthirificatedAtMs(sessionId, nowMs);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Ошибка БД при обновлении lastAuthirificatedAtMs для sessionId={}", sessionId, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем OK + storagePwd
|
|
||||||
Net_RefreshSession_Response resp = new Net_RefreshSession_Response();
|
Net_RefreshSession_Response resp = new Net_RefreshSession_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
@ -142,4 +175,13 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
|
|||||||
resp.setStoragePwd(session.getStoragePwd());
|
resp.setStoragePwd(session.getStoragePwd());
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String trimClientInfo(String info) {
|
||||||
|
if (info == null) return null;
|
||||||
|
info = info.trim();
|
||||||
|
if (info.length() > CLIENT_INFO_MAX_LEN) {
|
||||||
|
return info.substring(0, CLIENT_INFO_MAX_LEN);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -20,10 +20,13 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
* 1) AddUser — добавляем пользователя в локальную БД
|
* 1) AddUser — добавляем пользователя в локальную БД
|
||||||
* (loginKey и deviceKey разные).
|
* (loginKey и deviceKey разные).
|
||||||
*
|
*
|
||||||
* 2) AuthChallenge — запрашиваем sessionPwd.
|
* 2) AuthChallenge — запрашиваем одноразовый authNonce
|
||||||
|
* для подписи шаге 2.
|
||||||
*
|
*
|
||||||
* 3) CreateAuthSession — подтверждаем владение deviceKey,
|
* 3) CreateAuthSession — подтверждаем владение deviceKey,
|
||||||
* создаётся сессия, сервер возвращает sessionId (строка).
|
* создаётся сессия, сервер возвращает:
|
||||||
|
* - sessionId (строка, base64-32 байта)
|
||||||
|
* - sessionPwd (секрет сессии, base64-32 байта)
|
||||||
*
|
*
|
||||||
* 4) Новое подключение:
|
* 4) Новое подключение:
|
||||||
* - отправляем RefreshSession с тем же sessionId,
|
* - отправляем RefreshSession с тем же sessionId,
|
||||||
@ -48,6 +51,9 @@ public class Test_AddUser_and_Authorification {
|
|||||||
private static final long TEST_BCH_ID = 4222L;
|
private static final long TEST_BCH_ID = 4222L;
|
||||||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
private static final int TEST_BCH_LIMIT = 1_000_000;
|
||||||
|
|
||||||
|
// Краткая строка clientInfo, которую клиент шлёт на шаге CreateAuthSession
|
||||||
|
private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";
|
||||||
|
|
||||||
// --- Тестовые пары ключей ---
|
// --- Тестовые пары ключей ---
|
||||||
// loginKey — ключ аккаунта (например, "основной")
|
// loginKey — ключ аккаунта (например, "основной")
|
||||||
// deviceKey — ключ устройства, которым подписываем авторизацию
|
// deviceKey — ключ устройства, которым подписываем авторизацию
|
||||||
@ -72,12 +78,15 @@ public class Test_AddUser_and_Authorification {
|
|||||||
|
|
||||||
// --- Глобальные переменные между сценариями ---
|
// --- Глобальные переменные между сценариями ---
|
||||||
|
|
||||||
/** sessionPwd, выданный на шаге AuthChallenge. */
|
/** authNonce, выданный на шаге AuthChallenge. */
|
||||||
private static String GLOBAL_SESSION_PWD;
|
private static String GLOBAL_AUTH_NONCE;
|
||||||
|
|
||||||
/** sessionId (строка, base64-32 байта), выданный на шаге CreateAuthSession. */
|
/** sessionId (строка, base64-32 байта), выданный на шаге CreateAuthSession. */
|
||||||
private static String GLOBAL_SESSION_ID;
|
private static String GLOBAL_SESSION_ID;
|
||||||
|
|
||||||
|
/** sessionPwd (секрет сессии), выданный на шаге CreateAuthSession. */
|
||||||
|
private static String GLOBAL_SESSION_PWD;
|
||||||
|
|
||||||
/** storagePwd, который мы отправили при CreateAuthSession (для информации). */
|
/** storagePwd, который мы отправили при CreateAuthSession (для информации). */
|
||||||
private static String GLOBAL_STORAGE_PWD_SENT;
|
private static String GLOBAL_STORAGE_PWD_SENT;
|
||||||
|
|
||||||
@ -107,7 +116,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
WebSocket ws = client.newWebSocketBuilder()
|
client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new Listener() {
|
.buildAsync(URI.create(WS_URI), new Listener() {
|
||||||
|
|
||||||
private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2
|
private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2
|
||||||
@ -138,7 +147,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
}
|
}
|
||||||
case 2 -> {
|
case 2 -> {
|
||||||
GLOBAL_STORAGE_PWD_SENT = generateFakeStoragePwd();
|
GLOBAL_STORAGE_PWD_SENT = generateFakeStoragePwd();
|
||||||
String json = buildAuthStep2Json(GLOBAL_SESSION_PWD, GLOBAL_STORAGE_PWD_SENT);
|
String json = buildAuthStep2Json(GLOBAL_AUTH_NONCE, GLOBAL_STORAGE_PWD_SENT);
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("📤 [S1 / Шаг 3] Отправляем CreateAuthSession (подпись deviceKey):");
|
System.out.println("📤 [S1 / Шаг 3] Отправляем CreateAuthSession (подпись deviceKey):");
|
||||||
System.out.println(json);
|
System.out.println(json);
|
||||||
@ -160,17 +169,19 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println(message);
|
System.out.println(message);
|
||||||
System.out.println("-----------------------------------------------------");
|
System.out.println("-----------------------------------------------------");
|
||||||
|
|
||||||
// Шаг 2: получаем sessionPwd
|
// Шаг 2: получаем authNonce
|
||||||
if (step == 1) {
|
if (step == 1) {
|
||||||
GLOBAL_SESSION_PWD = extractSessionPwd(message);
|
GLOBAL_AUTH_NONCE = extractAuthNonce(message);
|
||||||
System.out.println("🔑 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD);
|
System.out.println("🔑 [S1] Извлечён authNonce: " + GLOBAL_AUTH_NONCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 3: получаем sessionId
|
// Шаг 3: получаем sessionId и sessionPwd
|
||||||
if (step == 2) {
|
if (step == 2) {
|
||||||
GLOBAL_SESSION_ID = extractSessionId(message);
|
GLOBAL_SESSION_ID = extractSessionId(message);
|
||||||
|
GLOBAL_SESSION_PWD = extractSessionPwd(message);
|
||||||
System.out.println("🆔 [S1] Извлечён sessionId: " + GLOBAL_SESSION_ID);
|
System.out.println("🆔 [S1] Извлечён sessionId: " + GLOBAL_SESSION_ID);
|
||||||
System.out.println(" (Эта sessionId и sessionPwd понадобятся в сценариях 2 и 3)");
|
System.out.println("🔐 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD);
|
||||||
|
System.out.println(" (Эти sessionId и sessionPwd понадобятся в сценариях 2 и 3)");
|
||||||
}
|
}
|
||||||
|
|
||||||
step++;
|
step++;
|
||||||
@ -221,7 +232,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
// Специально подменяем пароль, чтобы сервер его НЕ принял
|
// Специально подменяем пароль, чтобы сервер его НЕ принял
|
||||||
String wrongPwd = GLOBAL_SESSION_PWD + "_WRONG";
|
String wrongPwd = GLOBAL_SESSION_PWD + "_WRONG";
|
||||||
|
|
||||||
WebSocket ws = client.newWebSocketBuilder()
|
client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new Listener() {
|
.buildAsync(URI.create(WS_URI), new Listener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -281,7 +292,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ sessionPwd ===");
|
System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ sessionPwd ===");
|
||||||
System.out.println("Ожидаем УСПЕШНЫЙ ответ сервера (status=200),");
|
System.out.println("Ожидаем УСПЕШНЫЙ ответ сервера (status=200),");
|
||||||
System.out.println(" а в payload должен вернуться актуальный storagePwd (по твоей схеме).");
|
System.out.println(" а в payload должен вернуться актуальный storagePwd.");
|
||||||
|
|
||||||
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
||||||
System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 3.");
|
System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 3.");
|
||||||
@ -291,7 +302,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
WebSocket ws = client.newWebSocketBuilder()
|
client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new Listener() {
|
.buildAsync(URI.create(WS_URI), new Listener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -318,7 +329,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println("💬 [S3] Если status=200 — сессия успешно восстановлена.");
|
System.out.println("💬 [S3] Если status=200 — сессия успешно восстановлена.");
|
||||||
String storagePwdFromServer = extractStoragePwd(message);
|
String storagePwdFromServer = extractStoragePwd(message);
|
||||||
System.out.println("🧾 [S3] storagePwd от сервера: " + storagePwdFromServer);
|
System.out.println("🧾 [S3] storagePwd от сервера: " + storagePwdFromServer);
|
||||||
System.out.println(" (Может совпадать с тем, что был в шаге 2, или быть обновлённым — зависит от логики сервера)");
|
System.out.println(" (Должен совпадать с тем, что отправляли в шаге 3 сценария 1)");
|
||||||
|
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done");
|
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done");
|
||||||
webSocket.request(1);
|
webSocket.request(1);
|
||||||
@ -375,7 +386,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Шаг 1 авторизации: запрос sessionPwd
|
// 2) Шаг 1 авторизации: запрос authNonce
|
||||||
private static String buildAuthStep1Json() {
|
private static String buildAuthStep1Json() {
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
@ -388,11 +399,15 @@ public class Test_AddUser_and_Authorification {
|
|||||||
""".formatted(TEST_LOGIN);
|
""".formatted(TEST_LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Шаг 2 авторизации: подтверждение подписью
|
/**
|
||||||
// payload: storagePwd, timeMs, signatureB64
|
* 3) Шаг 2 авторизации: подтверждение подписью.
|
||||||
private static String buildAuthStep2Json(String sessionPwd, String storagePwd) {
|
*
|
||||||
if (sessionPwd == null) {
|
* @param authNonce одноразовый nonce с шага 1
|
||||||
sessionPwd = "";
|
* @param storagePwd клиентский storagePwd
|
||||||
|
*/
|
||||||
|
private static String buildAuthStep2Json(String authNonce, String storagePwd) {
|
||||||
|
if (authNonce == null) {
|
||||||
|
authNonce = "";
|
||||||
}
|
}
|
||||||
if (storagePwd == null || storagePwd.isBlank()) {
|
if (storagePwd == null || storagePwd.isBlank()) {
|
||||||
storagePwd = generateFakeStoragePwd();
|
storagePwd = generateFakeStoragePwd();
|
||||||
@ -400,8 +415,8 @@ public class Test_AddUser_and_Authorification {
|
|||||||
|
|
||||||
long timeMs = System.currentTimeMillis();
|
long timeMs = System.currentTimeMillis();
|
||||||
|
|
||||||
// preimage = "AUTHORIFICATED:" + timeMs + sessionPwd
|
// preimage = "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
String preimageStr = "AUTHORIFICATED:" + timeMs + sessionPwd;
|
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// Подписываем приватным ключом устройства (deviceKey)
|
// Подписываем приватным ключом устройства (deviceKey)
|
||||||
@ -415,13 +430,15 @@ public class Test_AddUser_and_Authorification {
|
|||||||
"payload": {
|
"payload": {
|
||||||
"storagePwd": "%s",
|
"storagePwd": "%s",
|
||||||
"timeMs": %d,
|
"timeMs": %d,
|
||||||
"signatureB64": "%s"
|
"signatureB64": "%s",
|
||||||
|
"clientInfo": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
storagePwd,
|
storagePwd,
|
||||||
timeMs,
|
timeMs,
|
||||||
sigB64
|
sigB64,
|
||||||
|
TEST_CLIENT_INFO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,13 +450,15 @@ public class Test_AddUser_and_Authorification {
|
|||||||
"requestId": "%s",
|
"requestId": "%s",
|
||||||
"payload": {
|
"payload": {
|
||||||
"sessionId": "%s",
|
"sessionId": "%s",
|
||||||
"sessionPwd": "%s"
|
"sessionPwd": "%s",
|
||||||
|
"clientInfo": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
requestId,
|
requestId,
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionPwd
|
sessionPwd,
|
||||||
|
TEST_CLIENT_INFO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,6 +475,19 @@ public class Test_AddUser_and_Authorification {
|
|||||||
// JSON HELPERS
|
// JSON HELPERS
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
|
|
||||||
|
private static String extractAuthNonce(String json) {
|
||||||
|
try {
|
||||||
|
JsonNode root = JSON_MAPPER.readTree(json);
|
||||||
|
JsonNode payload = root.get("payload");
|
||||||
|
if (payload != null && payload.has("authNonce")) {
|
||||||
|
return payload.get("authNonce").asText();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("⚠️ Не удалось распарсить authNonce из ответа: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String extractSessionPwd(String json) {
|
private static String extractSessionPwd(String json) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = JSON_MAPPER.readTree(json);
|
JsonNode root = JSON_MAPPER.readTree(json);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user