From b5c8884de48aa973ee0060eb2d91abce2096593f Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:51:18 +0900 Subject: [PATCH 01/13] =?UTF-8?q?chore:=20=ED=86=A0=ED=94=BD=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - topic/{roomId} -> topic/chat/{roomId} - 의미적 명확성을 위해 --- .../com/example/solidconnection/chat/config/StompHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index 660f01f28..ec1db0bcb 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -56,9 +56,9 @@ private String extractRoomId(String destination) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } String[] parts = destination.split("/"); - if (parts.length < 3 || !parts[1].equals("topic")) { + if (parts.length < 4 || !parts[1].equals("topic") || !parts[2].equals("chat")) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - return parts[2]; + return parts[3]; } } From 678ef9de8de23deff41a87df68c21b25e012b3c0 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:51:47 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/dto/ChatMessageSendRequest.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java new file mode 100644 index 000000000..92a18f5eb --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.dto; + +public record ChatMessageSendRequest( + long senderId, + String content +) { + +} From ecf92e3d9076250993d7ca7b0182c5066d69a557 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:54:20 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20Service=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatService.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index c378f6b50..f0536d57d 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -10,6 +10,7 @@ import com.example.solidconnection.chat.domain.ChatRoom; import com.example.solidconnection.chat.dto.ChatAttachmentResponse; import com.example.solidconnection.chat.dto.ChatMessageResponse; +import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.dto.ChatParticipantResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.dto.ChatRoomResponse; @@ -27,6 +28,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +42,8 @@ public class ChatService { private final ChatReadStatusRepository chatReadStatusRepository; private final SiteUserRepository siteUserRepository; + private final SimpMessagingTemplate simpMessagingTemplate; + @Transactional(readOnly = true) public ChatRoomListResponse getChatRooms(long siteUserId) { // todo : n + 1 문제 해결 필요! @@ -109,13 +113,6 @@ private ChatMessageResponse toChatMessageResponse(ChatMessage message) { ); } - private void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); - } - } - @Transactional public void markChatMessagesAsRead(long siteUserId, long roomId) { ChatParticipant participant = chatParticipantRepository @@ -124,4 +121,27 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { chatReadStatusRepository.upsertReadStatus(roomId, participant.getId()); } + + @Transactional + public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long roomId) { + validateChatRoomParticipant(chatMessageSendRequest.senderId(), roomId); + + ChatMessage chatMessage = new ChatMessage( + chatMessageSendRequest.content(), + chatMessageSendRequest.senderId(), + chatRoomRepository.findById(roomId) + .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) + ); + + chatMessageRepository.save(chatMessage); + + simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessage); + } + + private void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); + } + } } From 4928e0c2df7ecfd00a205b2124e595584af116a7 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:54:30 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20Controller=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatMessageController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java new file mode 100644 index 000000000..0a566bd99 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.chat.controller; + +import com.example.solidconnection.chat.dto.ChatMessageSendRequest; +import com.example.solidconnection.chat.service.ChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ChatMessageController { + + private final ChatService chatService; + + @MessageMapping("/chat/{roomId}") + public void sendChatMessage( + @DestinationVariable Long roomId, + @Payload ChatMessageSendRequest chatMessageSendRequest + ) { + + chatService.sendChatMessage(chatMessageSendRequest, roomId); + } +} From 2e1db36185b8a103c3ebc28ea6ddbe3e37c7546e Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 19:57:04 +0900 Subject: [PATCH 05/13] =?UTF-8?q?chore:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=84=20RestController=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Controller=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatMessageController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 0a566bd99..b31c20982 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -6,9 +6,9 @@ import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.stereotype.Controller; -@RestController +@Controller @RequiredArgsConstructor public class ChatMessageController { From 500b664652d1c4bc0fbad17a5cbef35152f347ae Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 20:01:24 +0900 Subject: [PATCH 06/13] =?UTF-8?q?chore:=20WebSocket=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20HTTP=20?= =?UTF-8?q?=ED=95=B8=EB=93=9C=EC=85=B0=EC=9D=B4=ED=81=AC=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=EC=9D=84=20=EC=88=98=ED=96=89=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/security/config/SecurityConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java index 3667e9d84..12a31c5d6 100644 --- a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java @@ -62,6 +62,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers("/connect/**").authenticated() .requestMatchers("/admin/**").hasRole(ADMIN.name()) .anyRequest().permitAll() ) From 560e13cd872391c935b3d022f0b16339de53df8c Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:02:56 +0900 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=ED=95=B8=EB=93=9C=EC=85=B0?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=20=ED=9B=84=20Principal=EC=9D=84=20WebSocket?= =?UTF-8?q?=20=EC=84=B8=EC=85=98=EC=97=90=20=EC=A0=84=EB=8B=AC=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이에 컨트롤러 인자로 siteUserId를 받도록 하고, DTO에 senderId를 삭제한다. --- .../chat/config/CustomHandshakeHandler.java | 27 ++++++++++++++++ .../chat/config/SiteUserPrincipal.java | 11 +++++++ .../chat/config/StompHandler.java | 30 ++++++----------- .../chat/config/StompWebSocketConfig.java | 7 +++- .../config/WebSocketHandshakeInterceptor.java | 32 +++++++++++++++++++ .../controller/ChatMessageController.java | 7 ++-- .../chat/dto/ChatMessageSendRequest.java | 1 - .../chat/service/ChatService.java | 6 ++-- 8 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java create mode 100644 src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java create mode 100644 src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java diff --git a/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java new file mode 100644 index 000000000..6c3054355 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; +import java.util.Map; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.support.DefaultHandshakeHandler; + +// WebSocket 세션의 Principal을 결정한다. +@Component +public class CustomHandshakeHandler extends DefaultHandshakeHandler { + + @Override + protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, + Map attributes) { + + Object userAttribute = attributes.get("user"); + + if (userAttribute instanceof Principal) { + Principal principal = (Principal) userAttribute; + return principal; + } + + return super.determineUser(request, wsHandler, attributes); + } +} diff --git a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java new file mode 100644 index 000000000..0fe494506 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; + +public record SiteUserPrincipal(Long id, String email) implements Principal { + + @Override + public String getName() { + return this.email; + } +} diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index ec1db0bcb..7cea816c7 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -2,11 +2,9 @@ import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED; -import com.example.solidconnection.auth.token.JwtTokenProvider; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; +import java.security.Principal; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; @@ -15,42 +13,34 @@ import org.springframework.stereotype.Component; @Component -@RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { - private final JwtTokenProvider jwtTokenProvider; - @Override public Message preSend(Message message, MessageChannel channel) { final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); if (StompCommand.CONNECT.equals(accessor.getCommand())) { - Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED); + Principal user = accessor.getUser(); + if (user == null) { + throw new CustomException(AUTHENTICATION_FAILED); + } } if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { - Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED); + SiteUserPrincipal user = (SiteUserPrincipal) accessor.getUser(); + if (user == null) { + throw new CustomException(AUTHENTICATION_FAILED); + } - String email = claims.getSubject(); String destination = accessor.getDestination(); - String roomId = extractRoomId(destination); - // todo: roomId 기반 실제 구독 권한 검사 로직 추가 + // todo: roomId와 user.getId() 기반으로 실제 구독 권한 검사 로직 } return message; } - private Claims validateAndExtractClaims(StompHeaderAccessor accessor, ErrorCode errorCode) { - String bearerToken = accessor.getFirstNativeHeader("Authorization"); - if (bearerToken == null || !bearerToken.startsWith("Bearer ")) { - throw new CustomException(errorCode); - } - String token = bearerToken.substring(7); - return jwtTokenProvider.parseClaims(token); - } - private String extractRoomId(String destination) { if (destination == null) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); diff --git a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java index 86b6eef5d..78baf8a13 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java @@ -22,12 +22,17 @@ public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { private final StompHandler stompHandler; private final StompProperties stompProperties; private final CorsProperties corsProperties; + private final WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; + private final CustomHandshakeHandler customHandshakeHandler; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { List strings = corsProperties.allowedOrigins(); String[] allowedOrigins = strings.toArray(String[]::new); - registry.addEndpoint("/connect").setAllowedOrigins(allowedOrigins).withSockJS(); + registry.addEndpoint("/connect") + .setAllowedOrigins(allowedOrigins) // postman 테스트를 위해 sockJS 비활성화 + .addInterceptors(webSocketHandshakeInterceptor) + .setHandshakeHandler(customHandshakeHandler); } @Override diff --git a/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java new file mode 100644 index 000000000..9e8aafe2d --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java @@ -0,0 +1,32 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; +import java.util.Map; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +// Principal을 WebSocket 세션에 저장하는 것에만 집중한다. +@Component +public class WebSocketHandshakeInterceptor implements HandshakeInterceptor { + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Map attributes) { + Principal principal = request.getPrincipal(); + + if (principal != null) { + attributes.put("user", principal); + return true; + } + + return false; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Exception exception) { + } +} diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index b31c20982..87436a422 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -2,6 +2,7 @@ import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.service.ChatService; +import com.example.solidconnection.common.resolver.AuthorizedUser; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -17,9 +18,9 @@ public class ChatMessageController { @MessageMapping("/chat/{roomId}") public void sendChatMessage( @DestinationVariable Long roomId, - @Payload ChatMessageSendRequest chatMessageSendRequest + @Payload ChatMessageSendRequest chatMessageSendRequest, + @AuthorizedUser Long siteUserId ) { - - chatService.sendChatMessage(chatMessageSendRequest, roomId); + chatService.sendChatMessage(chatMessageSendRequest, siteUserId, roomId); } } diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java index 92a18f5eb..623053268 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.chat.dto; public record ChatMessageSendRequest( - long senderId, String content ) { diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index f0536d57d..c08184ed9 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -123,12 +123,12 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { } @Transactional - public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long roomId) { - validateChatRoomParticipant(chatMessageSendRequest.senderId(), roomId); + public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long siteUserId, long roomId) { + validateChatRoomParticipant(siteUserId, roomId); ChatMessage chatMessage = new ChatMessage( chatMessageSendRequest.content(), - chatMessageSendRequest.senderId(), + siteUserId, chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) ); From 482b594184945fd31aaf2299477d9bdc4f19d1ad Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:30:26 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20Principal=EB=A5=BC=20=EB=B0=9B=EA=B3=A0,?= =?UTF-8?q?=20=EC=9D=B4=ED=9B=84=20SiteUserDetails=EC=97=90=EC=84=9C=20sit?= =?UTF-8?q?eUserId=EB=A5=BC=20=EC=B6=94=EC=B6=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatMessageController.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 87436a422..1673e503c 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -2,7 +2,9 @@ import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.service.ChatService; -import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; +import java.security.Principal; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -19,8 +21,11 @@ public class ChatMessageController { public void sendChatMessage( @DestinationVariable Long roomId, @Payload ChatMessageSendRequest chatMessageSendRequest, - @AuthorizedUser Long siteUserId + Principal principal ) { - chatService.sendChatMessage(chatMessageSendRequest, siteUserId, roomId); + TokenAuthentication tokenAuthentication = (TokenAuthentication) principal; + SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal(); + + chatService.sendChatMessage(chatMessageSendRequest, siteUserDetails.getSiteUser().getId(), roomId); } } From db8f107fc35139717c1ff722d71bb1a0eb62ea90 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:39:07 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:=20DTO=EB=A5=BC=20=ED=86=B5=ED=95=B4?= =?UTF-8?q?=20=EC=88=9C=ED=99=98=EC=B0=B8=EC=A1=B0=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/ChatMessageSendResponse.java | 19 +++++++++++++++++++ .../chat/service/ChatService.java | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java new file mode 100644 index 000000000..065c7ba1c --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.chat.dto; + +import com.example.solidconnection.chat.domain.ChatMessage; + +public record ChatMessageSendResponse( + long messageId, + String content, + long senderId +) { + + public static ChatMessageSendResponse from(ChatMessage chatMessage) { + return new ChatMessageSendResponse( + chatMessage.getId(), + chatMessage.getContent(), + chatMessage.getSenderId() + ); + } + +} diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index c08184ed9..8ecb1cf78 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -11,6 +11,7 @@ import com.example.solidconnection.chat.dto.ChatAttachmentResponse; import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatMessageSendRequest; +import com.example.solidconnection.chat.dto.ChatMessageSendResponse; import com.example.solidconnection.chat.dto.ChatParticipantResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.dto.ChatRoomResponse; @@ -135,7 +136,9 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long chatMessageRepository.save(chatMessage); - simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessage); + ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); + + simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } private void validateChatRoomParticipant(long siteUserId, long roomId) { From 5b1304d303827e892927562ebbc53db477734dcc Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:58:24 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore:=20=EC=8B=A4=EC=A0=9C=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=EA=B6=8C=ED=95=9C=20TODO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검증 로직이 핸들러에서 사용됨에 따라 발생하는 순환 참조를 막기 위해 Lazy 어노테이션을 사용한 생성자를 직접 작성 --- .../chat/config/StompHandler.java | 16 +++++++++++++--- .../chat/service/ChatService.java | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index 7cea816c7..a93a95270 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -2,9 +2,13 @@ import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED; +import com.example.solidconnection.chat.service.ChatService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; import java.security.Principal; +import lombok.RequiredArgsConstructor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; @@ -13,8 +17,11 @@ import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { + private final ChatService chatService; + @Override public Message preSend(Message message, MessageChannel channel) { final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); @@ -27,15 +34,18 @@ public Message preSend(Message message, MessageChannel channel) { } if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { - SiteUserPrincipal user = (SiteUserPrincipal) accessor.getUser(); + Principal user = accessor.getUser(); if (user == null) { throw new CustomException(AUTHENTICATION_FAILED); } + TokenAuthentication tokenAuthentication = (TokenAuthentication) user; + SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal(); + String destination = accessor.getDestination(); - String roomId = extractRoomId(destination); + long roomId = Long.parseLong(extractRoomId(destination)); - // todo: roomId와 user.getId() 기반으로 실제 구독 권한 검사 로직 + chatService.validateChatRoomParticipant(siteUserDetails.getSiteUser().getId(), roomId); } return message; diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 8ecb1cf78..56bba8306 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -26,14 +26,13 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; -import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@RequiredArgsConstructor @Service public class ChatService { @@ -45,6 +44,20 @@ public class ChatService { private final SimpMessagingTemplate simpMessagingTemplate; + public ChatService(ChatRoomRepository chatRoomRepository, + ChatMessageRepository chatMessageRepository, + ChatParticipantRepository chatParticipantRepository, + ChatReadStatusRepository chatReadStatusRepository, + SiteUserRepository siteUserRepository, + @Lazy SimpMessagingTemplate simpMessagingTemplate) { + this.chatRoomRepository = chatRoomRepository; + this.chatMessageRepository = chatMessageRepository; + this.chatParticipantRepository = chatParticipantRepository; + this.chatReadStatusRepository = chatReadStatusRepository; + this.siteUserRepository = siteUserRepository; + this.simpMessagingTemplate = simpMessagingTemplate; + } + @Transactional(readOnly = true) public ChatRoomListResponse getChatRooms(long siteUserId) { // todo : n + 1 문제 해결 필요! @@ -141,7 +154,7 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } - private void validateChatRoomParticipant(long siteUserId, long roomId) { + public void validateChatRoomParticipant(long siteUserId, long roomId) { boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); if (!isParticipant) { throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); From 865e520f69c9157c9edc28294ea2d3b3f8338179 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:59:38 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/security/config/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java index 12a31c5d6..706fedd52 100644 --- a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java @@ -5,8 +5,8 @@ import com.example.solidconnection.common.exception.CustomAccessDeniedHandler; import com.example.solidconnection.common.exception.CustomAuthenticationEntryPoint; import com.example.solidconnection.security.filter.ExceptionHandlerFilter; -import com.example.solidconnection.security.filter.TokenAuthenticationFilter; import com.example.solidconnection.security.filter.SignOutCheckFilter; +import com.example.solidconnection.security.filter.TokenAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; From ab6422f4266bd44b7fd43e2d6d04dd27aaecba73 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Mon, 4 Aug 2025 09:17:33 +0900 Subject: [PATCH 12/13] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20Si?= =?UTF-8?q?teUserPrincipal=20=EC=A0=9C=EA=B1=B0=20=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정규표현식을 사용하여 채팅방 ID 추출 - DTO 검증 추가 - 구체화 클래스가 아닌 인터페이스 사용하도록 (DIP) - senderId가 siteUserId가 아니라 chatParticipantId로 설정되도록 변경 --- .../chat/config/SiteUserPrincipal.java | 11 ------- .../chat/config/StompHandler.java | 11 +++++-- .../controller/ChatMessageController.java | 3 +- .../chat/dto/ChatMessageSendRequest.java | 5 ++++ .../chat/service/ChatService.java | 30 ++++++++++--------- 5 files changed, 31 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java diff --git a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java deleted file mode 100644 index 0fe494506..000000000 --- a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.solidconnection.chat.config; - -import java.security.Principal; - -public record SiteUserPrincipal(Long id, String email) implements Principal { - - @Override - public String getName() { - return this.email; - } -} diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index a93a95270..2e99bf9c4 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -8,6 +8,8 @@ import com.example.solidconnection.security.authentication.TokenAuthentication; import com.example.solidconnection.security.userdetails.SiteUserDetails; import java.security.Principal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -20,6 +22,7 @@ @RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { + private static final Pattern ROOM_ID_PATTERN = Pattern.compile("^/topic/chat/(\\d+)$"); private final ChatService chatService; @Override @@ -55,10 +58,12 @@ private String extractRoomId(String destination) { if (destination == null) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - String[] parts = destination.split("/"); - if (parts.length < 4 || !parts[1].equals("topic") || !parts[2].equals("chat")) { + + Matcher matcher = ROOM_ID_PATTERN.matcher(destination); + if (!matcher.matches()) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - return parts[3]; + + return matcher.group(1); } } diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 1673e503c..a7e158224 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -4,6 +4,7 @@ import com.example.solidconnection.chat.service.ChatService; import com.example.solidconnection.security.authentication.TokenAuthentication; import com.example.solidconnection.security.userdetails.SiteUserDetails; +import jakarta.validation.Valid; import java.security.Principal; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -20,7 +21,7 @@ public class ChatMessageController { @MessageMapping("/chat/{roomId}") public void sendChatMessage( @DestinationVariable Long roomId, - @Payload ChatMessageSendRequest chatMessageSendRequest, + @Valid @Payload ChatMessageSendRequest chatMessageSendRequest, Principal principal ) { TokenAuthentication tokenAuthentication = (TokenAuthentication) principal; diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java index 623053268..22d652a35 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -1,6 +1,11 @@ package com.example.solidconnection.chat.dto; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record ChatMessageSendRequest( + @NotNull(message = "메시지를 입력해주세요.") + @Size(max = 500, message = "메시지는 500자를 초과할 수 없습니다") String content ) { diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 56bba8306..fadd284fe 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,20 +42,20 @@ public class ChatService { private final ChatReadStatusRepository chatReadStatusRepository; private final SiteUserRepository siteUserRepository; - private final SimpMessagingTemplate simpMessagingTemplate; + private final SimpMessageSendingOperations simpMessageSendingOperations; public ChatService(ChatRoomRepository chatRoomRepository, ChatMessageRepository chatMessageRepository, ChatParticipantRepository chatParticipantRepository, ChatReadStatusRepository chatReadStatusRepository, SiteUserRepository siteUserRepository, - @Lazy SimpMessagingTemplate simpMessagingTemplate) { + @Lazy SimpMessageSendingOperations simpMessageSendingOperations) { this.chatRoomRepository = chatRoomRepository; this.chatMessageRepository = chatMessageRepository; this.chatParticipantRepository = chatParticipantRepository; this.chatReadStatusRepository = chatReadStatusRepository; this.siteUserRepository = siteUserRepository; - this.simpMessagingTemplate = simpMessagingTemplate; + this.simpMessageSendingOperations = simpMessageSendingOperations; } @Transactional(readOnly = true) @@ -107,6 +107,13 @@ public SliceResponse getChatMessages(long siteUserId, long return SliceResponse.of(content, chatMessages); } + public void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); + } + } + private ChatMessageResponse toChatMessageResponse(ChatMessage message) { List attachments = message.getChatAttachments().stream() .map(attachment -> ChatAttachmentResponse.of( @@ -138,11 +145,13 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { @Transactional public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long siteUserId, long roomId) { - validateChatRoomParticipant(siteUserId, roomId); + long senderId = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId) + .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND)) + .getId(); ChatMessage chatMessage = new ChatMessage( chatMessageSendRequest.content(), - siteUserId, + senderId, chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) ); @@ -151,13 +160,6 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); - simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); - } - - public void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); - } + simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } } From 338780fb0aea08347e2f224adce964e5600c4108 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Mon, 4 Aug 2025 09:42:48 +0900 Subject: [PATCH 13/13] =?UTF-8?q?chore:=20withSockJS=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/config/StompWebSocketConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java index 78baf8a13..51259a0e1 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java @@ -30,9 +30,10 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { List strings = corsProperties.allowedOrigins(); String[] allowedOrigins = strings.toArray(String[]::new); registry.addEndpoint("/connect") - .setAllowedOrigins(allowedOrigins) // postman 테스트를 위해 sockJS 비활성화 + .setAllowedOrigins(allowedOrigins) .addInterceptors(webSocketHandshakeInterceptor) - .setHandshakeHandler(customHandshakeHandler); + .setHandshakeHandler(customHandshakeHandler) + .withSockJS(); } @Override