From 1540780cdfdc655a8009f03ec97c2747fff5f927 Mon Sep 17 00:00:00 2001 From: gulnaz Date: Thu, 13 Mar 2025 00:26:18 +0300 Subject: [PATCH] SUPPORT-8957: add websocket service --- backend/pom.xml | 8 ++ backend/src/main/java/AppConfig.java | 8 ++ .../ErvuJwtAuthenticationProvider.java | 7 +- .../websocket/WebSocketConfig.java | 18 +++ .../websocket/dto/ProcessErrorMsg.java | 10 ++ .../websocket/dto/ProcessResponseBody.java | 12 ++ .../websocket/dto/ProcessResponseDto.java | 12 ++ .../websocket/enums/ClassName.java | 31 +++++ .../handler/ClientSocketHandler.java | 110 ++++++++++++++++++ .../websocket/service/WebSocketService.java | 51 ++++++++ pom.xml | 11 ++ 11 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/WebSocketConfig.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/handler/ClientSocketHandler.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/service/WebSocketService.java diff --git a/backend/pom.xml b/backend/pom.xml index 513a5dd9..0db16a65 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -216,6 +216,14 @@ org.postgresql postgresql + + org.springframework + spring-websocket + + + org.springframework + spring-messaging + javax.validation validation-api diff --git a/backend/src/main/java/AppConfig.java b/backend/src/main/java/AppConfig.java index 2e4a06a4..23dba5ef 100644 --- a/backend/src/main/java/AppConfig.java +++ b/backend/src/main/java/AppConfig.java @@ -1,6 +1,8 @@ import java.time.Duration; import javax.sql.DataSource; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import liquibase.integration.spring.SpringLiquibase; import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; @@ -81,4 +83,10 @@ public class AppConfig { public RestTemplate restTemplate() { return new RestTemplate(); } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true); + } } diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/security/provider/ErvuJwtAuthenticationProvider.java b/backend/src/main/java/ru/micord/ervu/account_applications/security/provider/ErvuJwtAuthenticationProvider.java index 198cd59b..b57b6a7a 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/security/provider/ErvuJwtAuthenticationProvider.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/security/provider/ErvuJwtAuthenticationProvider.java @@ -8,14 +8,18 @@ import ru.micord.ervu.account_applications.security.model.jwt.UserSession; import ru.micord.ervu.account_applications.security.model.jwt.authentication.JwtTokenAuthentication; import ru.micord.ervu.account_applications.security.model.jwt.authentication.JwtTokenDummy; import ru.micord.ervu.account_applications.security.service.JwtTokenService; +import ru.micord.ervu.account_applications.websocket.service.WebSocketService; @Component public class ErvuJwtAuthenticationProvider implements AuthenticationProvider { private final JwtTokenService jwtTokenService; + private final WebSocketService webSocketService; - public ErvuJwtAuthenticationProvider(JwtTokenService jwtTokenService) { + public ErvuJwtAuthenticationProvider(JwtTokenService jwtTokenService, + WebSocketService webSocketService) { this.jwtTokenService = jwtTokenService; + this.webSocketService = webSocketService; } @Override @@ -23,6 +27,7 @@ public class ErvuJwtAuthenticationProvider implements AuthenticationProvider { JwtTokenDummy jwtTokenDummy = (JwtTokenDummy) authentication; String jwtToken = jwtTokenDummy.getToken(); UserSession userSession = jwtTokenService.getUserSession(jwtToken); + webSocketService.connectToSocket(); return new JwtTokenAuthentication(userSession, jwtToken); } diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/WebSocketConfig.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/WebSocketConfig.java new file mode 100644 index 00000000..853a6ab8 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/WebSocketConfig.java @@ -0,0 +1,18 @@ +package ru.micord.ervu.account_applications.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +/** + * @author gulnaz + */ +@Configuration +public class WebSocketConfig { + + @Bean + public WebSocketClient webSocketStompClient() { + return new StandardWebSocketClient(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java new file mode 100644 index 00000000..f6736468 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java @@ -0,0 +1,10 @@ +package ru.micord.ervu.account_applications.websocket.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author gulnaz + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProcessErrorMsg(String message) { +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java new file mode 100644 index 00000000..4523e130 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java @@ -0,0 +1,12 @@ +package ru.micord.ervu.account_applications.websocket.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author gulnaz + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProcessResponseBody(String type, String userName, @JsonProperty("value") String tempPass, + String secretLink, ProcessErrorMsg msg) { +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java new file mode 100644 index 00000000..321fa20f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java @@ -0,0 +1,12 @@ +package ru.micord.ervu.account_applications.websocket.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import ru.micord.ervu.account_applications.websocket.enums.ClassName; + +/** + * @author gulnaz + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProcessResponseDto(String traceId, String forUser, ClassName className, + ProcessResponseBody body) { +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java new file mode 100644 index 00000000..3d0f3cb3 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java @@ -0,0 +1,31 @@ +package ru.micord.ervu.account_applications.websocket.enums; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author gulnaz + */ +public enum ClassName { + @JsonProperty("update") + UPDATE, + @JsonProperty("processError") + PROCESS_ERROR, + @JsonEnumDefaultValue + SKIP + +// private String value; +// +// ClassName(String value) { +// this.value = value; +// } +// +// public static ClassName getByValue(String value) { +// return Arrays.stream(ClassName.values()) +// .filter(e -> e.value.equals(value)) +// .findFirst() +// .orElse(ClassName.SKIP); +// } +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/handler/ClientSocketHandler.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/handler/ClientSocketHandler.java new file mode 100644 index 00000000..6421af16 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/handler/ClientSocketHandler.java @@ -0,0 +1,110 @@ +package ru.micord.ervu.account_applications.websocket.handler; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import ru.micord.ervu.account_applications.service.UserApplicationListService; +import ru.micord.ervu.account_applications.websocket.dto.ProcessResponseDto; +import ru.micord.ervu.account_applications.websocket.service.WebSocketService; + +/** + * @author gulnaz + */ +@Component +public class ClientSocketHandler extends TextWebSocketHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(TextWebSocketHandler.class); + + private final List sessions = new CopyOnWriteArrayList<>(); + + private final ObjectMapper objectMapper; + private final UserApplicationListService applicationService; + private final WebSocketService webSocketService; + + public ClientSocketHandler(ObjectMapper objectMapper, + UserApplicationListService applicationService, + @Lazy WebSocketService webSocketService) { + this.objectMapper = objectMapper; + this.applicationService = applicationService; + this.webSocketService = webSocketService; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + LOGGER.info("established connection {}", session); + sessions.add(session); + } + + @Override + public void handleMessage(WebSocketSession session, WebSocketMessage message) + throws JsonProcessingException { + ProcessResponseDto dto = objectMapper.readValue(message.getPayload().toString(), + ProcessResponseDto.class); + LOGGER.info("received message, type = {}, sessionId = {}, principal = {}", + dto.className(), session.getId(), session.getPrincipal()); + String traceId = dto.traceId(); + + switch (dto.className()) { + case UPDATE -> { + LOGGER.info("update by traceId = {}", traceId); + String tempPass = dto.body().tempPass(); + + if (StringUtils.hasText(tempPass)) { + applicationService.savePassword(traceId, tempPass); + } + else { + applicationService.saveAcceptedStatus(traceId); + } + } + case PROCESS_ERROR -> { + LOGGER.error("error by traceId = {}", traceId); + applicationService.saveError(traceId, dto.body().msg().message()); + } + } + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + LOGGER.info("received text message {}", message.getPayload()); + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) { + LOGGER.error("Transport error {}", exception.getMessage()); + + try { + session.close(); + } + catch (IOException e) { + LOGGER.error("Failed to close session on handleTransportError ", e); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + LOGGER.info("Connection closed"); + + if (!status.equals(CloseStatus.NORMAL)) { + try { + session.close(); + } + catch (IOException e) { + LOGGER.error("Failed to close session on afterConnectionClosed ", e); + } + } + sessions.remove(session); + webSocketService.connectToSocket(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/service/WebSocketService.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/service/WebSocketService.java new file mode 100644 index 00000000..93f3de98 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/service/WebSocketService.java @@ -0,0 +1,51 @@ +package ru.micord.ervu.account_applications.websocket.service; + +import java.net.URI; +import java.util.concurrent.ExecutionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.WebSocketClient; + +/** + * @author gulnaz + */ +@Service +public class WebSocketService { + private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketService.class); + + private final WebSocketClient webSocketClient; + private final WebSocketHandler webSocketHandler; + + @Value("${ervu.socket.url:wss://ervu-uat.test.gosuslugi.ru/service/notifier/gateway/notify/notifier.message.send.push}") + private String socketUrl; + + public WebSocketService(WebSocketClient webSocketClient, + WebSocketHandler webSocketHandler) { + this.webSocketClient = webSocketClient; + this.webSocketHandler = webSocketHandler; + } + + public void connectToSocket() { + WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); + headers.set("Content-Type", "application/json"); +// headers.add("Authorization", "Bearer " + securityContext.getToken()); + headers.add("Authorization", "Bearer " + getToken()); + + try { + webSocketClient.doHandshake(webSocketHandler, headers, URI.create(socketUrl)).get(); + } + catch (InterruptedException | ExecutionException e) { + LOGGER.error("Failed to connect socket"); + } + } + + private String getToken() { + return + "eyJraWQiOiJzc29rZXkxIiwidHlwIjoiSldUIiwiYWxnIjoiUlM1MTIifQ.eyJzdWIiOiI3MDhjMGJkNS05MDk0LTQyZjUtYWQwZS0xZWUyMTBlZTM5MDMiLCJhbXIiOlsiSURQX0xPQ0FMIl0sInJvbGVzIjpbInNlY3VyaXR5X2FkbWluaXN0cmF0b3IiLCLQkdCw0LfQvtCy0LDRjyDRgNC-0LvRjCDQtNC70Y8g0LDQstGC0L7RgNC40LfQsNGG0LjQuCIsItCf0L7Qu9GM0LfQvtCy0LDRgtC10LvRjNGB0LrQsNGPINGA0L7Qu9GMIiwiUzNDbGllbnQiXSwiaXNzIjoiaHR0cHM6Ly9lcnZ1LXVhdC1zc28udGVzdC5nb3N1c2x1Z2kucnUiLCJncm91cHMiOltdLCJkb21haW5faWQiOiI1NTNiOTQ0OS02NDYwLTQ4YzQtODhkNC1mMTgyODNhYmM0YTciLCJhdWQiOiJhcm0iLCJhY2NvdW50SWQiOiI3MDhjMGJkNS05MDk0LTQyZjUtYWQwZS0xZWUyMTBlZTM5MDMiLCJuYmYiOjAsImF6cCI6ImFybSIsImF1dGhfdGltZSI6MCwibmFtZSI6ItCX0LDRgNC40L_QvtCyINCt0LzQuNC70YwiLCJyZWFsbSI6ItCS0L7RgdGM0LzQvtC1INGD0L_RgNCw0LLQu9C10L3QuNC1INCT0Kgg0JLQoSDQoNCkIiwicG9zaXRpb24iOiLQnNC40LrQvtGA0LQg0J_QntCY0JEiLCJleHAiOjE3NDE4MjM4NDgsInNlc3Npb25fc3RhdGUiOiI4M2UyMzdmNS05NzQzLTQ4ZmMtYjY2MS1jN2ZhODc1NjcyNDQiLCJpYXQiOjE3NDE4MDk0NDgsIlVTRVJfSVAiOiIxNzIuMjYuMjcuMTAifQ.em1QK3Tux2WUCxk-ii0I6VApr7KKsKcnWaM7mPfS2T3VEyn9pwZuLo9KQsIaQ1kIf-xsdz3iwuWruyHn0KxUlJQB_s7gAcfLMPy_2XVwydb--XviwvZ9ZRpQbdb_uhoAojhO9_h_UGuQOaKwF3K_g4EfgyWnD9xHyrujLlV9HIoipH6Eixci9jhscelWXv76wP8sbyPyeB9YrKnWTtFyhCvMb3y3FjwSCDs7Hi7JPh0SDQUQ-o8z0h6mOPIuoo53S03AEVr40f3s9nr0APq7HoU3UW3kQfkEI9A060pqO3c2ItnJU5-FE-og0wMVEnEvXeZtHebsSP3IdssJDpKrNg"; + } +} diff --git a/pom.xml b/pom.xml index ff92feb8..082d4a7f 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ UTF-8 false + 5.3.39 2.9.2 3.192.3 72000 @@ -286,6 +287,16 @@ ${webbpm-platform.version} runtime + + org.springframework + spring-websocket + ${spring.version} + + + org.springframework + spring-messaging + ${spring.version} + javax.validation validation-api