eunja511005 / AutoCoding

0 stars 0 forks source link

안드로이드 STOMP Client, 스프링 STOMP Server 구현 #137

Open eunja511005 opened 9 months ago

eunja511005 commented 9 months ago

안드로이드 앱에서 스프링 STOMP 서비스 호출

접근 허용 추가

image

서버 소스

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // "/topic"으로 시작 하는 하위 주제 구독하는 클라이언트에게 메시지 전달
        config.setApplicationDestinationPrefixes("/app"); // "/app"으로 시작하는 목적지로 메시지 수신
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-service").setAllowedOriginPatterns("*").withSockJS(); // 클라이언트에서 "/ws-service"으로 연결할 수 있도록 설정
    }
}

 

import java.util.LinkedList;
import java.util.Queue;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.servlet.ModelAndView;

import com.eun.tutorial.dto.chat.ChatMessage;

@RestController
public class ChatController {

    private final Queue<DeferredResult<ChatMessage>> chatQueue = new LinkedList<>();

    @GetMapping("/chat/list")
    public ModelAndView list() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("jsp/main/content/chat");
        return modelAndView;
    }

    @MessageMapping("/chat/send")
    @SendTo("/topic/chat")
    public ChatMessage sendChatMessage(@Payload ChatMessage message) {
        // 받은 메시지를 "/topic/chat" 주제로 브로드캐스팅하여 모든 클라이언트에게 전송
        return message;
    }

    @MessageMapping("/book/send")
    @SendTo("/topic/book")
    public BookMessage sendBookMessage(@Payload BookMessage message) {
        // 받은 메시지를 "/topic/book" 주제로 브로드캐스팅하여 모든 클라이언트에게 전송
        return message;
    }
}

### JWT 토큰 발행
![image](https://github.com/eunja511005/AutoCoding/assets/118089135/55bb5011-cffc-46d3-8f7f-3b567a7b19a6)
***
![image](https://github.com/eunja511005/AutoCoding/assets/118089135/aaded36a-5fbc-4d47-9a47-a15e5c5f640f)

### 모바일 유저 추가
mobileUser/jw0713!@
eunja511005 commented 9 months ago

안드로이드 클라이언트

settings.gradle 변경

image

모듈 빌드 추가

    // stomp
    implementation 'com.github.NaikSoftware:StompProtocolAndroid:1.6.6'

    // rx
    implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

    // gson
    implementation 'com.google.code.gson:gson:2.8.8'

AndroidManifest.xml 권한 추가 및 WSS말고 WS 통신도 가능토록 설정

image

클라이언트 샘플(주의 : URL 마지막에 무조건 /websocket 붙여 줘야 함)

package com.example.chat;

import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

import com.google.gson.Gson;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import ua.naiksoftware.stomp.Stomp;
import ua.naiksoftware.stomp.StompClient;
import io.reactivex.disposables.Disposable;

public class MainActivity extends AppCompatActivity {

    private static final String SERVER_URL = "ws://192.168.219.106:8080/ws-chat/websocket";

    private TextView textViewStatus;
    private Button buttonSubscribe;
    private TextView textViewMessage;

    private StompClient mStompClient;
    private Disposable stompLifecycleDisposable;

    private final String TAG = this.getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textViewStatus = findViewById(R.id.textViewStatus);
        buttonSubscribe = findViewById(R.id.buttonSubscribe);
        textViewMessage = findViewById(R.id.textViewMessage);

        try {
            URI serverUri = new URI(SERVER_URL);

            mStompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, SERVER_URL);

            // 연결 설정 및 연결
            mStompClient.connect();

            // 연결이 성공한 경우
            stompLifecycleDisposable = mStompClient.lifecycle().subscribe(lifecycleEvent -> {
                switch (lifecycleEvent.getType()) {
                    case OPENED:
                        Log.d(TAG, "STOMP connection opened");

                        // 연결 성공 후 구독
                        mStompClient.topic("/topic/chat").subscribe(topicMessage -> {
                            String message = topicMessage.getPayload();
                            // 메시지를 UI에 표시하거나 처리
                            runOnUiThread(() -> textViewMessage.setText(message));
                        });

                        // 메시지 전송
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
                        String currentTime = sdf.format(new Date());

                        Message socketMessage = new Message();
                        socketMessage.setSender("YW");
                        socketMessage.setTimestamp(currentTime);
                        socketMessage.setMessage("My first STOMP message!");

                        // ChatMessage 객체를 JSON 문자열로 변환
                        String jsonMessage = new Gson().toJson(socketMessage);

                        mStompClient.send("/app/chat/send", jsonMessage).subscribe();

                        break;
                    case CLOSED:
                        Log.d(TAG, "STOMP connection closed");
                        break;
                    case ERROR:
                        Log.e(TAG, "Error in STOMP connection", lifecycleEvent.getException());
                        break;
                }
            });

        } catch (URISyntaxException e) {
            e.printStackTrace();
            Log.e(TAG, "WebSocket connection error", e);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 액티비티가 종료될 때 STOMP 클라이언트 연결 해제
        if (stompLifecycleDisposable != null) {
            stompLifecycleDisposable.dispose();
        }

        // STOMP 클라이언트 연결 해제
        if (mStompClient != null) {
            mStompClient.disconnect();
        }
    }
}
eunja511005 commented 9 months ago

JWT 얻기 위한 소스

리턴값이 JSON 형태 아니어도 오류 안나도록 GJON 설정

토큰 리턴을 위해 구독 프레임워크 이용(토큰 응답이 오면 구독 하여 처리 하도록, CompositeDisposable과 연계됨)

image


image

안드로이드 클라이언트에서 STOMP 호출시 Header 추가 방법

        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("Authorization", "Bearer " + jwtToken);

        mStompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, SERVER_URL, headerMap);
        // 연결 설정 및 연결
        mStompClient.connect();

스프링 서버에서 STOMP Header 필터를 이용해 꺼내는 방법

MyFilterConfiguration.java에 아래 내용 추가

    @Bean
    public FilterRegistrationBean<WebSocketFilter> webSocketFilter() {
        FilterRegistrationBean<WebSocketFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new WebSocketFilter());
        registration.addUrlPatterns("/ws-service/*"); // Set the URL patterns for the filter
        registration.setName("WebSocketFilter");
        registration.setOrder(3); // Set the order in which the filter should be applied
        return registration;
    }
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.Policy;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import com.eun.tutorial.dto.ZthhErrorDTO;
import com.eun.tutorial.dto.main.MenuControlDTO;
import com.eun.tutorial.dto.main.UserRequestHistoryDTO;
import com.eun.tutorial.service.ZthhErrorService;
import com.eun.tutorial.service.main.MenuControlService;
import com.eun.tutorial.service.main.UserRequestHistoryService;
import com.eun.tutorial.util.AuthUtils;
import com.eun.tutorial.util.JwtTokenUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class WebSocketFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest request = ((HttpServletRequest) servletRequest);

        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            log.info("Header Name: " + headerName + ", Header Value: " + headerValue);
        }

        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            return;
        }

        String authToken = header.substring(7); // "Bearer " 이후의 토큰 부분 추출

        if (JwtTokenUtil.validateToken(authToken)) {
            // 토큰이 유효한 경우 데이터 반환
            String username = JwtTokenUtil.extractUsername(authToken);
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            // 토큰이 유효하지 않은 경우 연결 거부
            throw new IllegalStateException("Invalid JWT token");
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Do nothing
    }

    @Override
    public void destroy() {
        // Do nothing
    }

}
eunja511005 commented 9 months ago

ConstraintLayou 정렬 자바에서 동적으로 변경

image  

[중요] 제일 마지막에 꼭 아래 코드 필요

constraintSet.applyTo(constraintLayout);