wongakim-99 / websocket-chatting

웹소켓을 활용한 채팅 연습
0 stars 0 forks source link

웹소켓 채팅에 SpringSecurity + Jwt를 적용하여 보안강화 #7

Open wongakim-99 opened 1 month ago

wongakim-99 commented 1 month ago

이번 이슈에서 SpringSecurity와 JWT를 이용하여 Web 및 Websocket의 보안을 좀 더 강화하고, 이전 이슈에서의 복잡한 로직을 최소한 간소화 예정

wongakim-99 commented 1 month ago

어제 업로드한 전체 프로젝트 구조

1. WebSocketConfig(웹소켓 설정)

WebSocketConfig 는 스프링이 웹소켓을 활성화하고 메시지 브로커를 설정하는 핵심 클래스이다. 이 클래스는 웹소켓을 통해 클라이언트와 서버가 양방향 통신을 할 수 있게 해준다.


2. RedisConfig & EmbeddedRedisConfig (Redis 설정)

Redis 는 pub/sub 메시징을 통해 여러 클라이언트가 같은 채팅방에서 실시간으로 메시지를 공유할 수 있게 해준다. RedisConfig는 외부 Redis 서버와의 연결을 설정하고, EmbeddedRedisConfig는 테스트 환경에서 사용할 수 있는 임베디드 Redis 설정을 제공할 수 있다.


3. ChatController (채팅 메시지 처리)

ChatController는 클라이언트가 웹소켓을 통해 전송한 메시지를 받아서 처리하는 역할을 한다. 이 컨트롤러는 클라이언트와 서버 간 메시지 통신을 담당하며, 일반적으로 WebSocket으로부터 메시지를 수신하고 이를 브로커에게 전달한다.


4. ChatRoomController (채팅방 관리)

ChatRoomController는 채팅방 생성, 입장, 나가기 등 채팅방과 관련된 HTTP 요청을 처리. 웹소켓 연결 전 단계에서 사용자가 어느 채팅방에 입장할지를 결정하는 중요한 역할을 한다.


5. RedisSubscriber (Redis 구독자)

RedisSubscriber는 Redis pub/sub 메시지를 구독하여 메시지를 실시간으로 수신하는 역할을 한다. 클라이언트가 채팅방에 메시지를 보내면, Redis가 이를 다른 구독자에게 전송한다.


6. ChatRoomRepository (채팅방 저장소)

이 클래스는 채팅방을 저장하고 관리하는 역할을 한다. 채팅방 정보를 메모리나 데이터베이스에서 관리하며, 채팅방 목록을 제공하거나 새로운 채팅방을 생성하는 역할을 한다.


7. JwtTokenProvider (JWT 토큰 관리)

이 클래스는 JWT(Json Web Token) 인증 및 토큰 생성을 담당. 채팅 기능에서 사용자의 인증 상태를 확인하거나, 웹소켓 연결 전에 사용자를 인증하는 데 사용될 수 있음.


8. ChatMessage (메시지 DTO)

ChatMessage는 실제로 클라이언트와 서버 간에 주고받는 메시지의 구조를 정의한 DTO이다. 채팅방에서 주고받는 메시지의 타입, 내용, 송신자 등의 정보가 포함된다.


9. ChatRoom (채팅방 DTO)

ChatRoom은 각 채팅방의 정보를 저장하는 데이터 전송 객체이다. 채팅방의 ID, 이름 등을 저장하며, 채팅방 생성 시 사용된다.

※틀린부분 있으면 또 다시 수정할 예정

wongakim-99 commented 1 month ago

StompHandler

StompHandler는 WebSocket을 통해 들어오는 요청의 헤더에 담긴 JWT 토큰을 검증하는 역할을 하는 클래스. STOMP 프로토콜을 기반으로 동작하며, WebSocket 연결 및 메시지 전송 시에 보안 처리 역할을 담당.

동작 흐름

  1. 클라이언트가 WebSocket 연결을 시도하면 CONNECT 명령어가 서버로 전송됩니다.
  2. StompHandler의 preSend 메서드가 호출되어 WebSocket 헤더에서 JWT 토큰을 가져옵니다.
  3. JwtTokenProvider를 사용해 토큰을 검증합니다.
  4. 검증이 성공하면 WebSocket 연결이 계속되고, 실패하면 연결이 차단됩니다.

주요 코드

  1. ChannelInterceptor
    • 이 인터페이스는 WebSocket 메시지가 채널을 통과하기 전에 또는 이후에 특정 로직을 실행할 수 있게 합니다. 주로 메시지를 필터링하거나 인증 및 권한 처리를 하는 데 사용됩니다.


  1. preSend 메서드
    • 이 메서드는 WebSocket 메시지가 실제로 전송되기 전에 호출됩니다. 메시지의 헤더와 내용 등을 검사하고 필요한 처리를 할 수 있는 곳입니다.


  1. StompHeaderAccessor
    • StompHeaderAccessor는 STOMP 프로토콜과 관련된 헤더를 쉽게 접근할 수 있게 해주는 유틸리티 클래스입니다.
    • StompHeaderAccessor.wrap(message)로 메시지 객체를 감싸서 STOMP 헤더에 접근할 수 있게 합니다.


  1. accessor.getCommand() -STOMP 명령어를 확인합니다. 여기서는 CONNECT 명령어인지 확인합니다.
    • STOMP 명령어는 CONNECT, SEND, SUBSCRIBE 등 여러 가지가 있으며, WebSocket 연결을 설정할 때 사용되는 것이 CONNECT입니다.


  1. validateToken
    • accessor.getFirstNativeHeader("token")로 WebSocket 요청 헤더에서 JWT 토큰을 가져옵니다.
    • JwtTokenProvider 클래스의 validateToken 메서드를 사용해 이 토큰을 검증합니다.
    • JWT 검증이 실패하면 예외가 발생하고, 연결이 중단될 수 있습니다. 이를 통해 WebSocket을 통한 통신도 인증된 사용자만 접근할 수 있게 됩니다.


정리

StompHandler는 WebSocket 연결 시 JWT 토큰을 검증하여 인증된 사용자만 연결을 허용하는 보안 역할을 수행합니다. JwtTokenProvider와의 협업을 통해, 이 클래스는 WebSocket 통신의 중요한 인증 단계를 담당하고 있습니다.

wongakim-99 commented 1 month ago

EmbeddedRedisConfig

EmbeddedRedisConfig 클래스는 로컬 환경에서 내장형 Redis 서버를 실행하고 종료하는 설정을 담당합니다. 이 코드를 통해 개발 환경에서 실제 Redis 서버 없이도 테스트할 수 있도록 임베디드 Redis 서버를 사용합니다.

동작 흐름

  1. 애플리케이션 시작
    • 스프링 애플리케이션이 시작될 때, @PostConstruct에 의해 redisServer() 메서드가 호출되어 내장형 Redis 서버가 지정된 포트로 시작됩니다.


  1. 애플리케이션 종료
    • 애플리케이션이 종료될 때, @PreDestroy에 의해 stopRedis() 메서드가 호출되어 실행 중인 Redis 서버가 종료됩니다.


주요 코드

  1. @Profile("local")
    • 이 어노테이션은 해당 설정이 로컬 환경에서만 활성화되도록 설정합니다.
    • 스프링 프로필 중 local 프로필이 활성화될 때에만 이 설정이 적용됩니다. 즉, application.properties에서 spring.profiles.active=local로 설정한 경우에만 이 클래스가 동작하게 됩니다.
    • 실제 배포 환경이나 다른 환경에서는 이 설정이 적용되지 않도록 할 수 있습니다.


  1. @Configuration
    • 스프링의 설정 클래스임을 나타내는 어노테이션입니다. 이 클래스는 스프링이 관리하는 빈을 등록하고 설정하는 역할을 합니다.


  1. @Value("${spring.data.redis.port}")
    • application.properties 파일에 설정된 Redis 포트를 주입받습니다.
    • 예를 들어 spring.data.redis.port=6379과 같이 설정하면, 해당 포트 번호가 redisPort 변수에 주입됩니다.


  1. RedisServer
    • 임베디드 Redis 서버를 실행하기 위한 클래스입니다.
    • 이 클래스를 사용하면 애플리케이션이 실행될 때 Redis 서버를 시작하고, 애플리케이션이 종료될 때 Redis 서버를 종료할 수 있습니다.
    • Redis 서버가 실제로 설치되어 있지 않아도 이 클래스를 통해 Redis 환경을 시뮬레이션할 수 있습니다.


  1. @PostConstruct 메서드
    • @PostConstruct: 스프링 빈이 생성되고 의존성 주입이 완료된 후에 실행되는 메서드를 나타냅니다.
    • 이 메서드는 애플리케이션이 시작될 때 임베디드 Redis 서버를 시작합니다.
    • RedisServer(redisPort)는 설정 파일에서 주입받은 포트를 사용하여 Redis 서버를 시작합니다.
    • redisServer.start()는 실제로 Redis 서버를 시작하는 메서드입니다.


  1. @PreDestroy 메서드
    • @PreDestroy: 스프링 컨텍스트가 종료되기 직전에 실행되는 메서드를 나타냅니다.
    • 이 메서드는 애플리케이션이 종료될 때 임베디드 Redis 서버를 중지합니다.
    • redisServer.stop()을 호출하여 Redis 서버를 종료합니다. 이는 애플리케이션이 종료될 때 리소스를 정리하는 중요한 역할을 합니다.


정리

wongakim-99 commented 1 month ago

RedisConfig

RedisConfig 클래스는 Redis와의 연결 및 메시지 처리를 설정하는 매우 중요한 부분입니다. 이 코드는 Redis의 pub/sub 시스템을 설정하고, 메시지 처리, 리스너 등록, RedisTemplate 등을 정의합니다.

동작 흐름

  1. 채널 설정: channelTopic() 메서드에서 "chatroom"이라는 Redis 채널을 생성합니다. 이는 클라이언트들이 메시지를 주고받는 특정 채널을 지정하는 역할을 합니다.

  2. 리스너 설정: redisMessageListener() 메서드는 Redis에서 발행된 메시지를 구독하기 위한 리스너를 설정합니다. 이 리스너는 "chatroom" 채널에 발행된 메시지를 구독하고, 해당 메시지를 RedisSubscriber 클래스의 sendMessage 메서드로 전달합니다.

  3. 메시지 처리: listenerAdapter() 메서드에서는 RedisSubscriber 클래스의 sendMessage 메서드를 리스너로 등록합니다. 즉, Redis에서 발행된 메시지가 들어오면 sendMessage 메서드를 통해 처리됩니다.

  4. RedisTemplate 설정: Redis에 데이터를 저장하거나 가져올 때 사용하는 템플릿으로, 키와 값을 직렬화하는 방법을 설정합니다. 이 설정을 통해 Redis에 간단히 데이터를 저장하고 불러올 수 있습니다.


주요 코드

  1. ChannelTopic 설정
    @Bean
    public ChannelTopic channelTopic() {
    return new ChannelTopic("chatroom");
    }
    • ChannelTopic: Redis의 pub/sub 시스템에서 메시지가 발행되고 구독될 때 사용하는 채널입니다. 여기서는 "chatroom"이라는 이름의 채널을 사용합니다.
    • 이 채널을 통해 여러 클라이언트들이 메시지를 주고받게 됩니다. 메시지를 주고받을 때 특정 토픽(여기서는 "chatroom")을 지정하여 해당 토픽을 구독한 클라이언트들에게 메시지를 전송하게 됩니다.


  1. RedisMessageListenerContainer 설정
    @Bean
    public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory,
                                                          MessageListenerAdapter listenerAdapter,
                                                          ChannelTopic channelTopic) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.addMessageListener(listenerAdapter, channelTopic);
    return container;
    }
    • RedisMessageListenerContainer: Redis에서 발행된(pub) 메시지를 수신(sub)하는 리스너 컨테이너입니다. Redis에 발행된 메시지를 구독할 수 있는 리스너를 설정합니다.
    • connectionFactory: Redis 서버와의 연결을 관리하는 RedisConnectionFactory를 사용합니다.
    • listenerAdapter: 실제 메시지를 처리하는 구독자입니다.
    • channelTopic: 위에서 설정한 채널을 구독합니다. 여기서는 "chatroom" 토픽에 발행된 메시지를 처리합니다.
    • 이 설정은 Redis 서버로부터 "chatroom" 채널에서 발행된 메시지를 리스너로 전달해주는 역할을 한다.


  1. MessageListenerAdapter 설정
    @Bean
    public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
    return new MessageListenerAdapter(subscriber, "sendMessage");
    }
    • MessageListenerAdapter: Redis에서 발행된 메시지를 처리할 구독자(Subscriber)를 등록하는 역할을 합니다.
    • RedisSubscriber 클래스의 sendMessage 메서드가 호출됩니다.
    • Redis에서 "chatroom" 토픽에 메시지가 발행되면, 이 리스너가 이를 감지하여 sendMessage 메서드로 메시지를 전달합니다.
    • RedisSubscriber 클래스는 실제로 메시지를 수신하고 처리하는 역할을 합니다.
    • 이 클래스의 sendMessage 메서드는 Redis에서 발행된 메시지를 받아 처리하는 로직을 포함하고 있습니다.


  1. RedisTemplate 설정
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
    return redisTemplate;
    }
    • RedisTemplate: Redis에 데이터를 저장하고 읽어오는 데 사용하는 템플릿 클래스입니다. 데이터베이스의 JdbcTemplate과 비슷한 역할을 합니다.
    • connectionFactory: Redis 서버와 연결을 설정하는 RedisConnectionFactory를 사용합니다.
    • setKeySerializer: Redis의 키를 직렬화하는 방식을 설정합니다. 여기서는 String으로 직렬화합니다.
    • setValueSerializer: Redis의 값을 직렬화하는 방식을 설정합니다. 여기서는 Jackson2JsonRedisSerializer를 사용해 값을 JSON 형식으로 직렬화합니다.
    • 이 설정은 Redis에 데이터를 읽고 쓰는 과정을 쉽게 처리할 수 있도록 해줍니다.


정리

wongakim-99 commented 1 month ago

WebSecurityConfig

Spring Security를 사용하여 애플리케이션의 보안 설정을 정의하는 WebSecurityConfig 클래스입니다. 이 설정은 주로 WebSocket 채팅 애플리케이션에서 사용자 인증 및 권한을 처리하는 역할을 하며, 개발 및 테스트 환경에서는 In-Memory로 사용자 데이터를 저장해 간단히 인증을 구현하는 구조입니다.

동작 흐름

  1. In-Memory 사용자 생성:
    • 세 명의 사용자 (happydaddy, angrydaddy, guest)가 메모리 내에 생성되며, 각각 USER 또는 GUEST 권한을 부여받습니다.
    • 이 설정은 간단한 테스트용으로, 실제로 운영 환경에서는 데이터베이스와 연동해 사용자 정보를 관리해야 합니다.


  1. SecurityFilterChain 설정:
    • "/chat/**"로 시작하는 모든 요청은 USER 권한을 가진 사용자만 접근할 수 있습니다.
    • 로그인 페이지는 모든 사용자가 접근할 수 있으며, 다른 경로는 기본적으로 제한 없이 접근 가능합니다.


  1. 비밀번호 인코딩:
    • 테스트 목적으로 비밀번호 인코딩을 비활성화하여 평문 비밀번호를 사용합니다. 이는 보안적인 이유로 운영 환경에서는 반드시 교체해야 합니다.


주요 코드

  1. @Configuration 및 @EnableWebSecurity
    • @Configuration: 이 클래스가 스프링의 설정 클래스임을 나타냅니다. 이 클래스를 통해 보안 관련 설정을 정의합니다.
    • @EnableWebSecurity: Spring Security를 활성화하는 어노테이션으로, 애플리케이션에 보안 기능을 추가합니다.


  1. SecurityFilterChain 설정
    • csrf().disable(): CSRF (Cross-Site Request Forgery) 보호 기능을 비활성화합니다. WebSocket의 경우 CSRF 보호가 필요하지 않을 수 있기 때문에 비활성화합니다.
    • headers().frameOptions().sameOrigin(): 이 설정은 iframe을 사용할 때 동일 출처 정책을 설정합니다. 주로 H2 데이터베이스 콘솔을 사용할 때 동일 출처를 허용하기 위해 사용됩니다.
    • formLogin().permitAll(): 로그인 페이지는 인증 없이 누구나 접근할 수 있도록 허용합니다.
    • authorizeHttpRequests(): 요청별로 접근 권한을 설정합니다.
    • requestMatchers("/", "/chat/").hasRole("USER"): "/chat/" 경로로 시작하는 요청은 USER 권한을 가진 사용자만 접근할 수 있도록 설정합니다.
    • anyRequest().permitAll(): 그 외의 모든 요청은 누구나 접근 가능하게 설정합니다.
    • 이 보안 설정은 주로 WebSocket 채팅에 접근하기 위해 사용자 인증이 필요하며, 기본적으로 "/chat/**" 경로는 USER 권한을 가진 사용자만 사용할 수 있도록 제한하는 역할을 합니다.


  1. In-Memory UserDetailsService 설정
    • In-Memory 인증: UserDetailsService 인터페이스를 구현한 InMemoryUserDetailsManager를 사용하여 메모리 내에서 사용자 계정을 생성합니다. 이 방법은 간단한 테스트 및 개발 환경에서 사용되며, 실제 운영 환경에서는 데이터베이스로 교체해야 합니다.
    • User.withUsername(): happydaddy, angrydaddy, guest라는 세 개의 사용자를 생성하며, 각각 USER 또는 GUEST 역할(roles)을 가지고 있습니다.
    • {noop}1234: {noop}은 비밀번호 인코딩을 사용하지 않음을 의미합니다. 여기서는 테스트 목적으로 인코딩을 사용하지 않고, 비밀번호는 모두 "1234"입니다.
    • 이 설정을 통해 로그인 페이지에 접속하면 happydaddy, angrydaddy, guest 중 하나의 사용자로 로그인할 수 있습니다. USER 권한이 있는 사용자만 "/chat/**" 경로에 접근할 수 있고, GUEST는 제한됩니다.


  1. PasswordEncoder 설정
    • NoOpPasswordEncoder: 비밀번호 인코딩을 사용하지 않고 평문 그대로 비밀번호를 저장하고 사용합니다.
    • 이는 테스트 목적에만 적합하며, 실제 운영 환경에서는 안전하지 않기 때문에 BCrypt 등의 비밀번호 인코더를 사용해야 합니다.


정리

이 코드는 WebSocket 기반 채팅 애플리케이션에서 간단한 보안 설정을 적용하며, 특히 In-Memory 사용자 관리와 Spring Security를 통해 기본적인 인증 및 권한 부여를 설정합니다. 개발 및 테스트 목적으로 설계된 코드이므로, 실제 운영 환경에서는 데이터베이스와 연동하여 사용자 정보를 관리하고, 적절한 비밀번호 인코더를 사용하는 것이 필수적입니다.

wongakim-99 commented 1 month ago

WebSockConfig

이 WebSockConfig 클래스는 WebSocket 메시지 브로커를 설정하는 역할을 합니다. 이 코드는 STOMP 프로토콜을 사용하여 WebSocket 연결을 관리하고, 메시지를 전송하는 경로를 정의하며, STOMP 핸들러를 통해 WebSocket 요청의 보안 및 기타 처리 작업을 관리합니다.

동작 흐름

  1. 클라이언트와 서버의 WebSocket 연결 설정:
    • 클라이언트는 /ws-stomp 엔드포인트를 사용해 서버와 WebSocket 연결을 시작합니다.
    • 이때, SockJS를 사용하여 WebSocket을 지원하지 않는 브라우저에서도 연결할 수 있도록 설정합니다.


  1. 메시지 브로커 설정:
    • 클라이언트는 서버로 메시지를 발행할 때 /pub 경로로 메시지를 전송하고, 서버는 /sub 경로를 통해 클라이언트들에게 메시지를 브로드캐스트합니다.


  1. STOMP 메시지 처리:
    • 클라이언트에서 들어오는 STOMP 명령(연결, 메시지 전송 등)은 StompHandler에서 가로채고 처리합니다.
    • 이를 통해 JWT 토큰 검증 등 보안 처리가 가능합니다.


주요 코드

  1. @EnableWebSocketMessageBroker
    • 이 어노테이션은 스프링이 STOMP 기반 WebSocket 메시지 브로커를 활성화하도록 합니다.
    • STOMP는 WebSocket을 통한 메시지 전송에서 사용하는 서브 프로토콜로, 클라이언트와 서버 간의 메시지를 쉽게 주고받을 수 있게 합니다.


  1. StompHandler 의존성
    • StompHandler는 WebSocket 메시지의 채널을 가로채고, STOMP 명령어를 처리하는 클래스입니다.
    • 이 클래스는 주로 WebSocket 요청이 들어올 때 JWT 토큰 검증 같은 보안 처리를 수행합니다.
    • 이 stompHandler는 configureClientInboundChannel 메서드를 통해 클라이언트의 Inbound 채널에서 사용됩니다.


  1. configureMessageBroker
    • MessageBrokerRegistry를 사용하여 메시지 브로커를 설정합니다. 메시지 브로커는 클라이언트들 간의 메시지를 라우팅하는 역할을 합니다.

경로 요약


  1. registerStompEndpoints


  1. configureClientInboundChannel
    • 클라이언트에서 서버로 들어오는 채널(inbound channel)을 설정합니다.
    • ChannelRegistration을 사용하여 StompHandler를 인터셉터로 등록합니다. 이를 통해 클라이언트에서 들어오는 모든 STOMP 메시지를 StompHandler에서 가로채고 처리할 수 있습니다.
    • 예를 들어, 클라이언트가 WebSocket 연결을 시도하거나 메시지를 전송할 때, StompHandler에서 JWT 토큰 검증을 수행할 수 있습니다.


정리

wongakim-99 commented 1 month ago

ChatController

WebSocket을 통해 들어오는 채팅 메시지를 처리하고, 이를 Redis를 통해 다른 클라이언트에게 전달하는 역할을 합니다. 이 컨트롤러는 특히 WebSocket 통신과 Redis pub/sub 시스템을 사용하여 채팅 메시지를 실시간으로 관리합니다.

동작 흐름

  1. 메시지 수신: 클라이언트가 WebSocket을 통해 /chat/message 경로로 메시지를 전송하면, 이 컨트롤러의 message() 메서드가 호출됩니다.


  1. 사용자 인증: WebSocket 메시지의 헤더에 포함된 JWT 토큰에서 사용자의 닉네임을 추출하여, 해당 메시지의 sender 필드에 설정합니다.


  1. 입장 메시지 처리: 만약 메시지 타입이 ENTER라면, 시스템 메시지 형식으로 사용자 입장 알림 메시지를 자동 생성합니다.


  1. Redis로 메시지 발행: 최종적으로, WebSocket을 통해 받은 메시지를 Redis의 pub/sub 시스템을 사용해 channelTopic에 발행(publish)합니다. 이렇게 발행된 메시지는 Redis의 구독자들에게 실시간으로 전달됩니다.


주요 코드

  1. 의존성 주입 (RedisTemplate, JwtTokenProvider, ChannelTopic)
    • RedisTemplate<String, Object> redisTemplate: Redis 서버와의 통신을 담당하며, Redis에 데이터를 읽고 쓰는 기능을 제공합니다. 여기서는 WebSocket으로 받은 메시지를 Redis에 발행(publish)하는 데 사용됩니다.


  1. @MessageMapping("/chat/message")
    • @MessageMapping: WebSocket에서 들어오는 특정 경로의 메시지를 처리합니다.
    • /chat/message 경로로 클라이언트가 메시지를 전송하면 이 메서드가 호출됩니다.
    • 메시지는 ChatMessage 객체로 받고, 헤더에 포함된 JWT 토큰은 @Header("token")으로 받습니다.


  1. JWT 토큰에서 사용자 정보 추출
    • jwtTokenProvider.getUserNameFromJwt(token)을 호출하여 JWT 토큰에서 사용자 이름 또는 닉네임을 추출합니다.
    • 이 닉네임은 메시지의 sender 필드에 설정됩니다. 이렇게 하면 메시지를 보낸 사용자가 누구인지 알 수 있습니다.


  1. 채팅방 입장 메시지 처리
    • 메시지 타입이 ENTER인 경우, 즉 사용자가 채팅방에 입장했을 때, 입장 메시지를 자동으로 생성합니다.
    • message.setSender("[알림]"): 입장 메시지의 발신자는 특별한 시스템 메시지로 표시됩니다.
    • message.setMessage(nickname + "님이 입장하셨습니다."): 해당 사용자가 채팅방에 입장했다는 메시지를 생성합니다.


  1. Redis로 메시지 발행
    • redisTemplate.convertAndSend() 메서드를 사용하여 Redis의 채널을 통해 메시지를 발행합니다.
    • channelTopic.getTopic()에서 채널 토픽을 가져와, 해당 토픽에 메시지를 발행하면, 이 토픽을 구독하고 있는 다른 클라이언트들이 실시간으로 메시지를 받을 수 있습니다.
    • 예를 들어, 클라이언트가 /sub/chat/roomId와 같은 경로를 구독하고 있으면, 이 경로에 발행된 메시지를 실시간으로 수신하게 됩니다.


정리

이 컨트롤러는 WebSocket을 통해 받은 채팅 메시지를 처리하고, 이를 Redis의 pub/sub 시스템을 통해 다른 클라이언트들에게 전파하는 역할을 합니다. JWT 토큰을 사용해 사용자 인증을 처리하고, 채팅방에 입장한 사용자를 알리는 기능도 포함되어 있습니다.

wongakim-99 commented 1 month ago

ChatRoomController

이 ChatRoomController 클래스는 채팅방 관리와 사용자 정보 조회를 처리하는 컨트롤러입니다. 주요 기능은 새로운 채팅방을 생성하거나, 채팅방 목록을 조회하고, 특정 채팅방에 대한 정보를 제공하는 역할을 합니다. 또한, JWT 토큰을 생성해 사용자 정보를 제공하는 기능도 포함되어 있습니다.

동작 흐름

  1. 채팅방 관리:
    • ChatRoomRepository를 사용하여 새로운 채팅방을 생성하고, 채팅방 목록 및 특정 채팅방의 정보를 관리합니다. /chat/rooms 경로에서 모든 채팅방 목록을 JSON으로 조회할 수 있고, /chat/room/enter/{roomId} 경로에서 특정 채팅방에 입장할 수 있습니다.


  1. JWT 기반 사용자 정보 제공:
    • JwtTokenProvider를 통해 사용자의 이름을 기반으로 JWT 토큰을 생성합니다. 이 토큰은 클라이언트가 WebSocket을 통해 통신할 때 인증 정보를 전달하는 데 사용됩니다.


  1. 프론트엔드와의 통신:
    • 대부분의 메서드가 @ResponseBody를 통해 JSON 형태로 데이터를 반환하므로, 프론트엔드와 비동기 통신을 할 수 있습니다. 예를 들어, 채팅방 목록을 프론트엔드에서 동적으로 불러오거나, 특정 채팅방에 대한 정보를 조회할 수 있습니다.


주요 코드

  1. 의존성 주입 (ChatRoomRepository, JwtTokenProvider)
    • ChatRoomRepository: 채팅방 정보를 저장하고 관리하는 저장소 역할을 합니다. 이 저장소를 통해 채팅방 목록을 조회하거나 새로운 채팅방을 생성할 수 있습니다.
    • JwtTokenProvider: JWT 토큰을 생성하고 검증하는 클래스입니다. 여기서는 getUserInfo() 메서드에서 사용자 정보를 포함한 JWT 토큰을 생성합니다.


  1. getUserInfo() - 사용자 정보 조회 및 JWT 토큰 생성
    • @GetMapping("/user"): 현재 로그인한 사용자의 정보를 조회하는 메서드입니다.
    • Authentication 객체: Spring Security를 통해 현재 인증된 사용자 정보를 가져옵니다. auth.getName()을 통해 사용자의 이름을 가져옵니다.
    • JwtTokenProvider: 해당 사용자의 이름을 기반으로 JWT 토큰을 생성합니다.
    • 역할: 현재 사용자의 이름과 JWT 토큰을 JSON 형태로 반환합니다. 이는 주로 프론트엔드에서 로그인된 사용자의 정보를 필요로 할 때 사용됩니다.


정리

wongakim-99 commented 1 month ago

IndexController

이 IndexController는 매우 간단한 컨트롤러로, 사용자가 애플리케이션의 루트 경로 (/ 또는 /index)로 접근할 때 채팅방 목록 페이지로 리다이렉트시키는 역할을 합니다. 이 컨트롤러는 애플리케이션의 기본 페이지를 설정하는 기능을 담당합니다.

동작 흐름

  1. 사용자가 브라우저에서 루트 경로(http://localhost:8080/) 또는 /index 경로로 접근합니다.
  2. index() 메서드가 호출되고, 이 메서드는 redirect:/chat/room을 반환하여 사용자를 /chat/room 경로로 리다이렉트시킵니다.
  3. /chat/room 경로는 채팅방 목록 페이지로 이동하게 되며, 이는 이전에 설명한 ChatRoomController의 rooms() 메서드에 의해 처리됩니다.
wongakim-99 commented 1 month ago

DTO 패키지

dto(Data Transfer Object) 패키지에는 애플리케이션에서 사용되는 데이터를 클라이언트와 서버 간에 전송하기 위한 객체들이 정의되어 있습니다. 여기서는 채팅 메시지, 채팅방, 로그인 정보와 관련된 세 가지 DTO 클래스가 정의되어 있습니다.

1. ChatMessage

채팅 메시지와 관련된 정보를 다루는 DTO로, 메시지 타입, 채팅방 ID, 발신자, 메시지 내용을 포함합니다.

2. ChatRoom

채팅방 정보를 관리하는 DTO로, 채팅방 ID와 이름을 포함하며, 새 채팅방을 생성할 수 있습니다.

3. LoginInfo

로그인된 사용자 이름과 해당 사용자의 JWT 토큰을 제공하는 DTO입니다.

이 DTO들은 서버와 클라이언트 간 데이터를 주고받는 데 중요한 역할을 하며, 각 클래스는 특정 기능에 맞춰 데이터를 관리합니다.

wongakim-99 commented 1 month ago

RedisSubscriber

RedisSubscriber 클래스는 Redis pub/sub 시스템을 활용하여 Redis에서 발행된 메시지를 구독하고 처리하는 역할을 담당합니다. Redis에서 발행된 메시지를 받아서 이를 WebSocket을 통해 채팅방에 구독한 클라이언트들에게 전달하는 기능을 합니다.

동작 흐름

  1. Redis에서 메시지 발행: 클라이언트가 채팅 메시지를 전송하고, 해당 메시지는 Redis의 특정 채널에 발행됩니다.
  2. RedisSubscriber에서 메시지 수신: Redis에서 발행된 메시지를 RedisSubscriber가 구독하여 수신합니다.
  3. JSON 메시지를 객체로 변환: 수신한 메시지를 ObjectMapper로 ChatMessage 객체로 변환합니다.
  4. WebSocket 구독 경로로 메시지 전송: STOMP를 사용해 /sub/chat/room/{roomId} 경로로 메시지를 전송합니다.
  5. 클라이언트가 메시지 수신: 해당 채팅방을 구독 중인 클라이언트들이 실시간으로 메시지를 받습니다.


주요 코드

  1. 의존성 주입 (ObjectMapper, SimpMessageSendingOperations)
    • ObjectMapper: Jackson 라이브러리의 ObjectMapper는 JSON 데이터를 객체로 변환하거나 객체를 JSON으로 변환하는 데 사용됩니다. 여기서는 Redis에서 발행된 JSON 형식의 메시지를 ChatMessage 객체로 변환하는 데 사용됩니다.


  1. sendMessage(String publishMessage) 이 메서드는 Redis에서 발행된 메시지를 받아서 처리합니다.
ChatMessage chatMessage = objectMapper.readValue(publishMessage, ChatMessage.class);
messagingTemplate.convertAndSend("/sub/chat/room/" + chatMessage.getRoomId(), chatMessage);


정리

wongakim-99 commented 1 month ago

ChatRoomRepository

ChatRoomRepository 클래스는 Redis를 이용하여 채팅방을 저장하고 관리하는 역할을 수행합니다. 이 클래스는 Redis의 Hash 자료구조를 사용해 채팅방 정보를 저장하며, 채팅방 목록을 조회하거나 새로운 채팅방을 생성하는 기능을 제공합니다.

동작 흐름

  1. 채팅방 목록 조회 (findAllRoom()):
    • Redis에 저장된 모든 채팅방 목록을 조회하고, 이를 반환합니다.


  1. 특정 채팅방 조회 (findRoomById()):
    • 주어진 roomId로 Redis에 저장된 특정 채팅방을 조회하고, 그 정보를 반환합니다.


  1. 채팅방 생성 (createChatRoom()):
    • 새로운 채팅방을 생성하고, Redis의 Hash 자료구조에 저장합니다. 새로 생성된 채팅방 정보를 클라이언트에게 반환합니다.


주요 코드

  1. RedisTemplate & HashOperations
    • RedisTemplate<String, Object>: Spring에서 Redis와 상호작용하기 위한 템플릿 클래스입니다. Redis 서버에 데이터를 저장하거나 조회하는 데 사용됩니다.
    • 여기서는 String 타입의 키와 Object 타입의 값을 처리하는 RedisTemplate이 사용됩니다.
    • HashOperations<String, String, ChatRoom>: Redis의 Hash 자료구조와 상호작용하기 위한 인터페이스입니다.
    • Hash 자료구조는 Map처럼 키-값 쌍을 저장하는 방식입니다.
    • String 타입의 키와 ChatRoom 객체를 저장하는 구조로 되어 있으며, Redis에서 여러 채팅방을 관리하는 데 사용됩니다.


  1. @PostConstruct - 초기화
    • @PostConstruct: Spring 빈이 초기화된 후 호출되는 메서드입니다.
    • 여기서는 redisTemplate.opsForHash()를 사용하여 HashOperations 객체인 opsHashChatRoom을 초기화합니다. 이를 통해 Redis의 Hash 자료구조와 상호작용할 수 있습니다.


  1. createChatRoom(String name) - 채팅방 생성
    • 새로운 채팅방을 생성하고 Redis에 저장합니다.
    • ChatRoom.create(name): 주어진 이름으로 새로운 ChatRoom 객체를 생성합니다. 이때 UUID로 고유한 roomId가 생성됩니다.
    • opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom): 생성된 채팅방을 Redis의 Hash 자료구조에 저장합니다.
    • CHAT_ROOMS는 Redis의 Hash 이름이고, roomId는 해당 채팅방의 키로 사용되며, chatRoom 객체는 값으로 저장됩니다.
    • 생성된 chatRoom 객체를 반환하여, 클라이언트가 새로 생성된 채팅방 정보를 확인할 수 있습니다.


정리

wongakim-99 commented 1 month ago

JwtTokenProvide

JwtTokenProvider 클래스는 JWT(Json Web Token)를 생성하고, 검증하며, 복호화하여 사용자 정보를 얻는 역할을 합니다. JWT는 주로 사용자 인증과 세션 관리를 위해 사용되며, 이 클래스는 사용자의 이름을 포함한 토큰을 생성하고, 이를 검증하는 기능을 제공합니다.

동작 흐름

  1. JWT 생성 (generateToken):
    • 사용자 이름을 받아 JWT를 생성합니다. JWT에는 발행 시간과 만료 시간이 포함되며, HS256 알고리즘을 사용해 비밀키로 서명합니다.


  1. JWT에서 사용자 정보 추출 (getUserNameFromJwt):
    • 전달받은 JWT를 복호화하여, 그 안에 저장된 사용자 이름(ID)을 추출합니다.


  1. JWT 유효성 검증 (validateToken):
    • 전달된 JWT가 유효한지, 서명이 올바른지, 만료되지 않았는지 등을 검증합니다. 유효하지 않으면 예외를 발생시킵니다.


  1. 클레임 추출 및 예외 처리 (getClaims):
    • JWT를 파싱하여 클레임을 추출하고, 필요한 경우 예외를 처리하여 잘못된 토큰을 로그로 기록합니다.
wongakim-99 commented 1 month ago

전체 흐름

WebSocket을 이용한 실시간 채팅 기능을 구현하기 위한 다양한 구성 요소들이 어떻게 유기적으로 상호작용하는지 보여줌. 각 클래스는 서로 역할을 나누어 특정 기능을 담당하고 있으며, 이를 통해 JWT 기반 사용자 인증, Redis를 통한 실시간 메시지 처리, 그리거 WebSocket 통신이 가능

1. 사용자 인증 및 JWT 토큰 관리(JwtTokenProvide)

흐름 :


2. WebSocket 설정 및 STOMP 메시지 처리(WebSockConfig, StompHandler, ChatController)

흐름:


3. Redis 를 통한 실시간 메시지 전송(ChatRoomRepository, RedisSubscriber)

흐름:


4. 프론트엔드와의 상호작용(IndexController, ChatRoomController)