본문 바로가기

항해99

23.10.21 항해 99 16기 실전 프로젝트 16일차

오늘 공부한 것

* WebSocket 사용한 채팅 기능 구현 (Spring Security 및 JWT 도입)

 

오늘은 어제 게을러서 하지 않았던 Spring Securit 및 JWT를 Stomp에 도입하였다

 

1. Config

@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker // 웹소켓 서버를 사용한다는 설정
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    private final StompHandler stompHandler;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 서버 -> 클라이언트로 발행하는 메세지에 대한 endpoint 설정 : 구독
        config.enableSimpleBroker("/sub");
//        registry.enableSimpleBroker("/queue", "topic"); // 발행자가 queue (1:1) topic (1:다)의 경로로 메세지를 주면 구독자들에게 전달
        // 클라이언트 -> 서버로 발행하는 메세지에 대한 endpoint 설정 : 구독에 대한 메세지
        config.setApplicationDestinationPrefixes("/pub");
//        registry.setApplicationDestinationPrefixes("/app"); // 메세지 앞에 app이 붙어있는 경로로 발신되면 해당 경로를 처리하고 있는 핸들러로 전달됨 @MessageMapping 어노테이션이 붙은 곳을 타겟으로 한다는 설정
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 웹 소켓이 hanshake를 하기 위해 연결하는 endpoint
        registry.addEndpoint("/ws") // WebSocket 또는 SockJS가 웹소켓 핸드세이크 커넥션을 생성할 경로
                .setAllowedOriginPatterns("*")
                .withSockJS(); // WebSocket을 지원하지 않는 브라우저에서 HTTP의 Polling과 같은 방식으로 WebSocket의 요청을 수행하도록 도와줌
    }

    // 컨트롤러에 가기전 Stomp Message 객체를 가져와서 JWT 유효성 검사를하고 리턴시킴
    @Override
    public void configureClientInboundChannel (ChannelRegistration registration) {
        registration.interceptors(stompHandler);
    }
}

 

2. Controller

@RequiredArgsConstructor
@Controller
public class ChatController {

    private final SimpMessageSendingOperations messagingTemplate; //특정 Broker로 메세지를 전달
    private final JwtUtil jwtUtil;

    @MessageMapping("/chat/message")
    public void message(ChatMessageDto message, @Header("token") String token) {
        String nickname = jwtUtil.getUserInfoFromToken(token).getSubject();

        if (ChatMessageDto.MessageType.ENTER.equals(message.getType())) {
            message.setMessage(nickname + "님이 입장하셨습니다.");
            messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
        }
    }
}
@RequiredArgsConstructor
@Controller
@RequestMapping("/chat")
public class ChatRoomController {

    private final ChatRoomRepository chatRoomRepository;

    // 채팅 리스트 화면
    @GetMapping("/room")
    public String room(Model model) {
        return "/chat/room";
    }

    // 모든 채팅방 목록 반환
    @GetMapping("/rooms")
    @ResponseBody
    public List<ChatRoomDto> room() {
        return chatRoomRepository.findAllRoom();
    }

    // 채팅방 생성
    @PostMapping("/room")
    @ResponseBody
    public ChatRoomDto createRoom(@RequestParam String name) {
        return chatRoomRepository.createChatRoom(name);
    }

    // 채팅방 입장화면
    @GetMapping("/room/enter/{roomId}")
    public String roomDetail(Model model, @PathVariable String roomId) {
        model.addAttribute("roomId", roomId);
        return "/chat/roomdetail";
    }

    // 특정 채팅방 조회
    @GetMapping("room/{roomId}")
    @ResponseBody
    public ChatRoomDto roomInfo(@PathVariable String roomId) {
        return chatRoomRepository.findRoomById(roomId);
    }

}

 

3. Dto

@Getter
@Setter
public class ChatMessageDto {
    // 메세지 타입 : 입장, 채팅
    public enum MessageType{
        ENTER,

        TALK
    }

    private MessageType type; // 메세지 타입
    private String roomId; // 방번호
    private String sender; //메세지 보낸사람
    private String message; // 메세지
}
@Getter
@Setter
public class ChatRoomDto {
    private String roomId;
    private String name;

    public static ChatRoomDto create(String name) {
        ChatRoomDto chatRoomDto = new ChatRoomDto();
        chatRoomDto.roomId = UUID.randomUUID().toString(); // UUID : 네트워크 상에서 고유성이 보장되는 id
        chatRoomDto.name = name;
        return chatRoomDto;
    }
}

 

4. Handler

@Slf4j
@RequiredArgsConstructor
@Component
public class StompHandler implements ChannelInterceptor {

    private final JwtUtil jwtUtil;

    // websocket을 통해 들어온 요청이 처리 되기전 실행된다.
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        // websocket 연결시 헤더의 jwt token 검증
        if (StompCommand.CONNECT == accessor.getCommand()) {
            jwtUtil.validateToken(accessor.getFirstNativeHeader("token"));
        }
        return message;
    }
}

 

5. Repository

@Repository
public class ChatRoomRepository {

    private Map<String, ChatRoomDto> chatRoomDtoMap;

    // 의존하는 객체를 설정한 이후 초기화 작업을 수행하는 메서드에 적용
    @PostConstruct
    private void init() {
        chatRoomDtoMap = new LinkedHashMap<>();
    }

    public List<ChatRoomDto> findAllRoom(){
        // 채팅방 생성순서 최근 순으로 반환
        List chatRooms = new ArrayList<>(chatRoomDtoMap.values());
        Collections.reverse(chatRooms);
        return chatRooms;
    }

    public ChatRoomDto findRoomById(String id) {
        return chatRoomDtoMap.get(id);
    }

    public ChatRoomDto createChatRoom(String name) {
        ChatRoomDto chatRoomDto = ChatRoomDto.create(name);
        chatRoomDtoMap.put(chatRoomDto.getRoomId(), chatRoomDto);
        return chatRoomDto;
    }
}

 

6. 결과

    결과는 아래 보는대로 채팅방 개설에는 실패했다

    아마도 JWT 토큰을 확인해서 해당하는 유저만 개설할수 있게 만들어놔서 그런듯 하다

    원래라면 JWT 토큰 부분을 없애고 다시 테스트를 해보려고했는데 

    갑자기 컴파일 부분에서 트러블 슈팅이 일어나서 시간을 많이 잡아먹었다... 해당 트러블 슈팅은 따로 정리해두었다