[Stomp] Spring Boot 구현

ss0510s·2024년 5월 23일
0

WebSocket

목록 보기
3/5

[Stomp] Spring Boot 구현

설정

  • build.gradle 파일
    // socket
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
  • WebSocketConfiguration
    // 메시지 브로커가 지원하는 WebSocket 메시지 처리 활성화
    @EnableWebSocketMessageBroker
    @Configuration
    public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
    
    		// 소켓 연결과 관련된 설정
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/{endpoint}") // Handshake와 통신을 담당할 endpoint 지정
    								// cors 허용
                    .setAllowedOriginPatterns("로컬경로", "서버경로") // 서버 경로
                    .withSockJS(); // 소켓을 지원하지 않을 경우 대체
        }
    
    		// stomp 사용을 위한 메시지 브로커 설정
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
    				// 메시지를 보낼 때, 관련 경로를 설정해주는 함수
            config.setApplicationDestinationPrefixes("/pub");
    				// 메시지를 받을 때, 경로를 설정해주는 함수
    				/* **/sub** ,/topic”가 api에 prefix로 붙은 경우, **messageBroker**가 해당 경로를 가로챔 */
            config.enableSimpleBroker("/sub");
    				//  발행된 순서를 보존할지 결정
            config.setPreservePublishOrder(true);
        }
    
    		@Override
        public void configureClientInboundChannel(ChannelRegistration registration){
            registration.interceptors(stompHandler);
        }
    }

Controller 구현

@Controller
@RequiredArgsConstructor
@CrossOrigin(origins = "*", allowCredentials = "true")
@Slf4j
public class StompPersonalChatController {

		// 개인 메시지 전송
		//stompConfig에서 설정한 applicationDestinationPrefixes와 @MessageMapping 경로가 병합됨
    @MessageMapping(value = "/{endpoint}/send/{roomId}") // 메시지 전송 경로 설정
    public void sendMessage(@DestinationVariable String personalChatId, Message messageReq)
        throws IOException {

        log.debug("[StompChatController - sendMessage]: room id = {}", roomId);

        Message message = messageService.registMessage(roomId, messageReq);

        template.convertAndSend("/sub/" + roomId, message); // 구독중인 다른 사용자들에게 해당 메시지 전송
    }

Spring Security

  • jwt 인증시 websocket은 인증 방식을 다르게 설정하므로, 기존 jwt 인증 filter에서 websocket은 제외

기존 설정에서 제외

  • FilterChain.java
    @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
            String path = request.getRequestURI();
            if (!path.startsWith("/api") || path.matches("/api/.*/v0(/.*)?") || path.matches("/api/chat(/.*)?")) {
                filterChain.doFilter(request, response);
                return;
            }
    ....
  • SecurityConfig
    @Bean
        public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
            ...
    
            http
    					....
                .authorizeHttpRequests(auth ->
                    auth.requestMatchers("/api/*/v0/**").permitAll()
                        .requestMatchers("/api/*/v0").permitAll()
                            .requestMatchers("/api/chat/**").permitAll()
                        .anyRequest().authenticated()
                )
    					....;
    
            return http.build();
        }

Interceptor로 구현

  • WebSocketConfiguration
    	@Override
        public void configureClientInboundChannel(ChannelRegistration registration){
            registration.interceptors(stompHandler);
        }
  • StompHandler
    • WebSocket으로 오는 연결을 가로채서 해당 handler에서 토큰 검증
    • Connect시에 검증
@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE + 99) // 우선순위가 무엇보다 높음
public class StompHandler implements ChannelInterceptor {
    private final JWTUtil jwtUtil;
    private final APIUserDetailsService apiUserDetailsService;
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            Map<String, Object> payload = validateAccessToken(
                Objects.requireNonNull(accessor.getFirstNativeHeader("Authorization")));

            System.out.println(payload);
            String memberId = (String) payload.get("memberId");

            UserDetails userDetails = apiUserDetailsService.loadUserByUsername(memberId);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        return message;
    }
profile
개발자가 되기 위해 성장하는 중입니다.

0개의 댓글