Compare commits
1 Commits
multi-type
...
master
Author | SHA1 | Date | |
---|---|---|---|
7183812a79 |
8
pom.xml
8
pom.xml
@ -42,14 +42,6 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-graphql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-reactive-routes</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
|
36
src/main/java/dev/struchkov/example/CustomConfigurator.java
Normal file
36
src/main/java/dev/struchkov/example/CustomConfigurator.java
Normal file
@ -0,0 +1,36 @@
|
||||
package dev.struchkov.example;
|
||||
|
||||
import jakarta.websocket.HandshakeResponse;
|
||||
import jakarta.websocket.server.HandshakeRequest;
|
||||
import jakarta.websocket.server.ServerEndpointConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CustomConfigurator extends ServerEndpointConfig.Configurator {
|
||||
|
||||
@Override
|
||||
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
|
||||
final Map<String, List<String>> headers = request.getHeaders();
|
||||
final List<String> cookies = headers.get("cookie");
|
||||
|
||||
String sessionId = parseCookies(cookies); // ваша реализация парсинга кук
|
||||
config.getUserProperties().put("sessionId", sessionId);
|
||||
}
|
||||
|
||||
public String parseCookies(List<String> cookies) {
|
||||
if (cookies != null) {
|
||||
for (String cookie : cookies) {
|
||||
String[] singleCookie = cookie.split(";");
|
||||
for (String part : singleCookie) {
|
||||
part = part.trim();
|
||||
if (part.startsWith("sessionId")) {
|
||||
return part.substring("sessionId".length() + 1).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
package dev.struchkov.example;
|
||||
|
||||
import dev.struchkov.example.convert.EventContainerDecoder;
|
||||
import dev.struchkov.example.convert.EventContainerEncoder;
|
||||
import dev.struchkov.example.domain.EventContainer;
|
||||
import dev.struchkov.example.domain.input.ChatInputMessage;
|
||||
import dev.struchkov.example.domain.input.ChatViewInput;
|
||||
import dev.struchkov.example.domain.output.ChatOutputMessage;
|
||||
import dev.struchkov.example.domain.output.ChatViewOutput;
|
||||
import dev.struchkov.example.converter.ChatMessageDecoder;
|
||||
import dev.struchkov.example.converter.ChatMessageEncoder;
|
||||
import dev.struchkov.example.domain.ChatInputMessage;
|
||||
import dev.struchkov.example.domain.ChatOutputMessage;
|
||||
import io.vertx.ext.web.handler.HttpException;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.websocket.OnClose;
|
||||
import jakarta.websocket.OnError;
|
||||
@ -17,7 +15,6 @@ import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@ -28,57 +25,60 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@ApplicationScoped
|
||||
@ServerEndpoint(
|
||||
value = "/chat/{chatId}",
|
||||
decoders = EventContainerDecoder.class,
|
||||
encoders = EventContainerEncoder.class
|
||||
decoders = ChatMessageDecoder.class,
|
||||
encoders = ChatMessageEncoder.class,
|
||||
configurator = CustomConfigurator.class
|
||||
)
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocket {
|
||||
|
||||
public static final ThreadLocal<UUID> CURRENT_USER = new ThreadLocal<>();
|
||||
private final Map<String, List<Session>> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session, @PathParam("chatId") String chatId) {
|
||||
System.out.println("onOpen> " + chatId);
|
||||
final String authCookieValue = (String) session.getUserProperties().get("sessionId");
|
||||
final UUID authUserId = getAuthUser(authCookieValue);
|
||||
session.getUserProperties().put("userId", authUserId);
|
||||
sessions.computeIfAbsent(chatId, key -> new ArrayList<>()).add(session);
|
||||
}
|
||||
|
||||
private UUID getAuthUser(String authCookieValue) {
|
||||
// your auth logic here
|
||||
if (authCookieValue == null) throw new HttpException(401, "Не передан параметр авторизации.");
|
||||
if (authCookieValue.equals("user1")) return UUID.fromString("09e429de-a302-40b6-9d10-6b113ab9e89d");
|
||||
if (authCookieValue.equals("user2")) return UUID.fromString("f84dbae1-f9a9-4c37-8922-4eb207103676");
|
||||
throw new HttpException(403, "Пользователь не авторизован.");
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, @PathParam("chatId") String chatId, Throwable throwable) {
|
||||
if (throwable instanceof HttpException httpException) {
|
||||
final int statusCode = httpException.getStatusCode();
|
||||
if (statusCode == 401) {
|
||||
session.getAsyncRemote().sendText("Вы не авторизованы.");
|
||||
closeSession(session, chatId);
|
||||
return;
|
||||
}
|
||||
if (statusCode == 403) {
|
||||
session.getAsyncRemote().sendText("Доступ запрещен.");
|
||||
closeSession(session, chatId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
System.out.println("onError> " + chatId + ": " + throwable);
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session, @PathParam("chatId") String chatId) {
|
||||
System.out.println("onClose> " + chatId);
|
||||
closeSession(session, chatId);
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, @PathParam("chatId") String chatId, Throwable throwable) {
|
||||
System.out.println("onError> " + chatId + ": " + throwable);
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(Session session, @PathParam("chatId") String chatId, EventContainer event) {
|
||||
System.out.println("onMessage> " + chatId + ": " + event);
|
||||
switch (event.getEventType()) {
|
||||
case MESSAGE_NEW -> sendMessage(session, chatId, (ChatInputMessage) event.getEvent());
|
||||
case MESSAGE_VIEWED -> viewMessage(session, chatId, (ChatViewInput) event.getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
private void viewMessage(Session session, String chatId, ChatViewInput viewInput) {
|
||||
final List<Session> chatSessions = sessions.get(chatId);
|
||||
for (Session chatSession : chatSessions) {
|
||||
if (session.getId().equals(chatSession.getId())) {
|
||||
continue;
|
||||
}
|
||||
final UUID fromUserId = CURRENT_USER.get();
|
||||
final ChatViewOutput chatViewOutput = new ChatViewOutput(
|
||||
viewInput.getMessageId(),
|
||||
fromUserId,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
final EventContainer eventContainer = EventContainer.viewedOutput(chatViewOutput);
|
||||
chatSession.getAsyncRemote().sendObject(eventContainer);
|
||||
CURRENT_USER.remove();
|
||||
}
|
||||
public void onMessage(Session session, @PathParam("chatId") String chatId, ChatInputMessage message) {
|
||||
System.out.println("onMessage> " + chatId + ": " + message);
|
||||
sendMessage(session, chatId, message);
|
||||
}
|
||||
|
||||
private void sendMessage(Session session, String chatId, ChatInputMessage message) {
|
||||
@ -87,11 +87,9 @@ public class WebSocket {
|
||||
if (session.getId().equals(chatSession.getId())) {
|
||||
continue;
|
||||
}
|
||||
final UUID fromUserId = CURRENT_USER.get();
|
||||
final UUID fromUserId = (UUID) session.getUserProperties().get("userId");
|
||||
final ChatOutputMessage outputMessage = new ChatOutputMessage(fromUserId, message.getText());
|
||||
final EventContainer eventContainer = EventContainer.messageOutput(outputMessage);
|
||||
chatSession.getAsyncRemote().sendObject(eventContainer);
|
||||
CURRENT_USER.remove();
|
||||
chatSession.getAsyncRemote().sendObject(outputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,44 +0,0 @@
|
||||
package dev.struchkov.example.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import dev.struchkov.example.domain.EventContainer;
|
||||
import dev.struchkov.example.domain.EventType;
|
||||
import dev.struchkov.example.domain.input.ChatInputMessage;
|
||||
import dev.struchkov.example.domain.input.ChatViewInput;
|
||||
import jakarta.websocket.DecodeException;
|
||||
import jakarta.websocket.Decoder;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class EventContainerDecoder implements Decoder.Text<EventContainer> {
|
||||
|
||||
private final ObjectMapper jackson = EventContainerDecoder.getJackson();
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public EventContainer decode(String s) throws DecodeException {
|
||||
final String eventType = jackson.readTree(s).get("eventType").asText();
|
||||
final JsonNode event = jackson.readTree(s).get("event");
|
||||
return switch (EventType.valueOf(eventType)) {
|
||||
case MESSAGE_NEW -> EventContainer.messageInput(jackson.treeToValue(event, ChatInputMessage.class));
|
||||
case MESSAGE_VIEWED -> EventContainer.viewInput(jackson.treeToValue(event, ChatViewInput.class));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willDecode(String s) {
|
||||
return s != null;
|
||||
}
|
||||
|
||||
public static ObjectMapper getJackson() {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
om.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
om.registerModule(new JavaTimeModule());
|
||||
return om;
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package dev.struchkov.example.convert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.struchkov.example.domain.EventContainer;
|
||||
import jakarta.websocket.EncodeException;
|
||||
import jakarta.websocket.Encoder;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class EventContainerEncoder implements Encoder.Text<EventContainer> {
|
||||
|
||||
private final ObjectMapper jackson = EventContainerDecoder.getJackson();
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String encode(EventContainer eventContainer) throws EncodeException {
|
||||
return jackson.writeValueAsString(eventContainer);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package dev.struchkov.example.converter;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import dev.struchkov.example.domain.ChatInputMessage;
|
||||
import jakarta.websocket.DecodeException;
|
||||
import jakarta.websocket.Decoder;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class ChatMessageDecoder implements Decoder.Text<ChatInputMessage> {
|
||||
|
||||
private final ObjectMapper jackson = ChatMessageDecoder.getJackson();
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public ChatInputMessage decode(String s) throws DecodeException {
|
||||
return jackson.readValue(s, ChatInputMessage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willDecode(String s) {
|
||||
return s != null;
|
||||
}
|
||||
|
||||
public static ObjectMapper getJackson() {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
om.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
om.registerModule(new JavaTimeModule());
|
||||
return om;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.struchkov.example.converter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.struchkov.example.domain.ChatOutputMessage;
|
||||
import jakarta.websocket.EncodeException;
|
||||
import jakarta.websocket.Encoder;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public class ChatMessageEncoder implements Encoder.Text<ChatOutputMessage> {
|
||||
|
||||
private final ObjectMapper jackson = ChatMessageDecoder.getJackson();
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String encode(ChatOutputMessage chatOutputMessage) throws EncodeException {
|
||||
return jackson.writeValueAsString(chatOutputMessage);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package dev.struchkov.example.domain.input;
|
||||
package dev.struchkov.example.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
@ -1,4 +1,4 @@
|
||||
package dev.struchkov.example.domain.output;
|
||||
package dev.struchkov.example.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
@ -1,54 +0,0 @@
|
||||
package dev.struchkov.example.domain;
|
||||
|
||||
import dev.struchkov.example.domain.input.ChatInputMessage;
|
||||
import dev.struchkov.example.domain.input.ChatViewInput;
|
||||
import dev.struchkov.example.domain.output.ChatOutputMessage;
|
||||
import dev.struchkov.example.domain.output.ChatViewOutput;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class EventContainer {
|
||||
|
||||
private EventType eventType;
|
||||
private Object event;
|
||||
|
||||
public static EventContainer messageInput(ChatInputMessage chatInputMessage) {
|
||||
return EventContainer.builder()
|
||||
.eventType(EventType.MESSAGE_NEW)
|
||||
.event(chatInputMessage)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static EventContainer viewInput(ChatViewInput chatViewInput) {
|
||||
return EventContainer.builder()
|
||||
.eventType(EventType.MESSAGE_VIEWED)
|
||||
.event(chatViewInput)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static EventContainer viewedOutput(ChatViewOutput chatViewOutput) {
|
||||
return EventContainer.builder()
|
||||
.eventType(EventType.MESSAGE_VIEWED)
|
||||
.event(chatViewOutput)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static EventContainer messageOutput(ChatOutputMessage chatOutputMessage) {
|
||||
return EventContainer.builder()
|
||||
.eventType(EventType.MESSAGE_NEW)
|
||||
.event(chatOutputMessage)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package dev.struchkov.example.domain;
|
||||
|
||||
public enum EventType {
|
||||
|
||||
MESSAGE_NEW, MESSAGE_VIEWED
|
||||
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package dev.struchkov.example.domain.input;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class ChatViewInput {
|
||||
|
||||
private UUID messageId;
|
||||
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package dev.struchkov.example.domain.output;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public class ChatViewOutput {
|
||||
|
||||
private UUID messageId;
|
||||
private UUID chatMemberId;
|
||||
private LocalDateTime dateRead;
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package dev.struchkov.example.filter;
|
||||
|
||||
import dev.struchkov.example.WebSocket;
|
||||
import io.quarkus.vertx.web.RouteFilter;
|
||||
import io.vertx.core.http.Cookie;
|
||||
import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.handler.HttpException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class WebsocketAuthFilter {
|
||||
|
||||
@RouteFilter(100)
|
||||
void authFilter(RoutingContext rc) {
|
||||
final HttpServerRequest currentRequest = rc.request();
|
||||
|
||||
if (isWebsocketRequest(currentRequest)) {
|
||||
final Cookie authCookie = currentRequest.getCookie("sessionId");
|
||||
if (authCookie == null) {
|
||||
throw new HttpException(401, "Не передан параметр авторизации.");
|
||||
}
|
||||
|
||||
final String authValue = authCookie.getValue();
|
||||
if (!authLogic(authValue)) {
|
||||
throw new HttpException(403, "Пользователь не авторизован.");
|
||||
}
|
||||
}
|
||||
|
||||
rc.next();
|
||||
}
|
||||
|
||||
private static boolean isWebsocketRequest(HttpServerRequest currentRequest) {
|
||||
return currentRequest.headers().contains("Upgrade")
|
||||
&& "websocket".equals(currentRequest.getHeader("Upgrade"));
|
||||
}
|
||||
|
||||
private boolean authLogic(String sessionId) {
|
||||
// your auth logic here
|
||||
if (sessionId.equals("user1")) {
|
||||
WebSocket.CURRENT_USER.set(UUID.fromString("09e429de-a302-40b6-9d10-6b113ab9e89d"));
|
||||
return true;
|
||||
} else if (sessionId.equals("user2")) {
|
||||
WebSocket.CURRENT_USER.set(UUID.fromString("f84dbae1-f9a9-4c37-8922-4eb207103676"));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user