채팅 (STOMP + JWT)

YoungHo-Cha·2022년 5월 4일
20

운동 매칭 시스템

목록 보기
13/17

틀린 부분이 많아요! 참고하고 보셨으면 좋겠습니다.

지난 STOMP의 후속작이다. 이번 내용은 이전 프로젝트와는 다르게, 서버를 Rest 서버로 생성할 예정이다.

여기서부터 많이 복잡해진다.

목차

  • 요구사항
  • 고민
  • 구현 방향
  • 코드

요구사항

요구사항은 다음과 같다.

  1. 채팅방은 여러개가 존재한다.
  2. 채팅방에는 입장한 사람만 채팅이 가능하다.
  3. 채팅방에는 최대 인원이 존재한다.
  4. 채팅방 인원이 최대 시, 채팅방에 입장하지 못한다.
  5. 채팅방은 로그인이 되어있는 사용자만 입장이 가능하다.
  6. 채팅방 입장은 JWT를 이용하여 해당 사용자를 식별한다.
  7. Access Token 만료 시, Refresh 과정이 필요하다.
  8. Refresh Token 만료 시, 로그인 창으로 Redirect가 되어야 한다.
  9. JWT 인증이 완료되어야 소켓 연결이 이루어져야 한다.

고민

위의 요구사항을 구현하면서 많은 고민이 생겼다.

이전에 구현한 내용은 다음의 순서로 로직이 수행된다.

  1. 채팅방 입장
  2. 일단 소켓 연결 (/ws/chat)
  3. 연결 검증(handler)
  4. 연결 성공(connected)
  5. 구독(subscribe)
  6. 구독 요청 JWT 검증
    1. JWT 만료 시, 에러 Message 응답
  7. 메세지 발행(publish)
  8. 발행 요청 JWT 검증
    1. JWT 만료 시, 에러 Message 응답
  9. 채팅방 입장 시, 최근 20개의 채팅을 DB에서 조회하여 응답한다.

채팅방 입장

  • 채팅방을 입장하다는 행위를 어떻게 정의해야할까?

채팅방 입장부터 고민이 많았다. 채팅방이라는 Pool을 어떻게 정의를 할까..

  • DB에 그냥 등록하고 소켓 연결하면 되잖아?

맞다. 이렇게 구현하기로 했다.

그에 해당하는 로직을 팀원과 상의해서 다음과 같이 정의했다.

  1. 채팅방 “입장하기" 버튼 클릭(HTTP)
  2. 입장 가능 여부에 따른 로직 분기
    1. 입장 가능한 경우 : 입장 했다는 Code와 입장에 따른 필요 데이터 응답
    2. 입장 불가능한 경우 : 입장 못했다는 Code 응답

일단 소켓 연결

  • 소켓 연결은 당연히 입장에 성공했을 때!

입장 성공 응답을 받았을 때, 소켓 연결을 신청하는 것이다.

api 보안은?

우리 서비스는 JWT를 이용한다. 소켓을 이용할 때 또한 JWT를 이용하기로 했다.

JWT는 다음 3가지 상황에서 이용한다. (Header에 “Authorization” 추가)

  • 소켓 연결
  • 구독 요청
  • 메세지 발행

이제 하나씩 살펴보자!


소켓 연결 및 검증

우선 다음 그림을 보자.

위 그림을 보면

  • 연결
  • 통신
  • 연결 해제

로 세 가지의 상호 작용을 볼 수 있다.

  • 가장 처음의 통신은 HTTP이다.

소켓은 아주아주 편하다. 연결할 때는 HTTP로 수행되기 때문에 응답하기가 너무너무 편하다.

이미 구현해놓은 스프링 시큐리티 JWT 필터만 태우면 된다!

ok 소켓 연결 및 검증 부분 구상 완료.


구독 검증 및 발행 검증

사용자가 메세지를 보낼 때도 그 메세지가 인증된 사용자가 보냈는지 확인을 해야 한다. 그러기 위해서는 구독 및 발행 마다 해당 사용자를 검증해야한다.

그러기 위해서는 구독 및 발행 마다 Header에 “Authorization”을 추가하여 서버에 보내주어야 할 것이다. 그러기 위해서는 스프링에서는 어떻게 해야할까?

여기부터 복잡해진다. 소켓 연결과는 다르게 HTTP가 아닌 WS상에서 통신을 한다. 그래서 구현을 이미 해놓은 스프링 시큐리티 JWT 필터를 태우기가 애매하다.(jwt 필터는 HttpServletResponse 객체를 다루기 때문) 그래서 다른 인터셉터를 구현해야 한다.

어쨌든 !

STOMP도 헤더를 포함하기 때문에 Header에 Authorization을 추가하여 검증을 하기로 했다.

JWT가 만료되면 어떻게 하나?

여기에 엄청나게 많은 고민이 있었다.

임의로 구현하여 개발자 도구를 엄청나게 만져보니까 힌트를 얻었다. 다음 그림을 보자.

SEND 커맨드를 통해 메세지를 발행했다. 올바르지 않은 토큰에 대한 ERROR 커맨드 메세지를 응답하는 것을 볼 수 있다.

저 ERROR를 커스텀하여 프론트엔드에게 알려주자!!


STOMP + JWT in Spring Boot

먼저 나는 백엔드 직무를 하기 때문에 백엔드 로직만 다루겠다!

앞서 올린 글에 이어서 구현할 예정이다.

https://velog.io/@jkijki12/STOMP-Spring-Boot

구현 순서는 다음과 같다.

  1. 소켓 필터링 처리
  2. PreHandler 구현
  3. ErrorHandler 구현

소켓 필터링 처리

SecurityConfig 수정

.antMatchers("/api/websocket").authenticated()
  • .antMatchers("/api/websocket").authenticated() : 해당 URL로 요청이 올 경우 JWT 검사를 수행한다는 내용이다.

소켓은 완료되었다.

PreHandler 구현

PreHandler란?

메세지를 발행하기 전, 메세지에 대한 전처리를 수행하는 Handler이다.

Spring 에서는 STOMP 메세지에 대한 전처리의 커스텀을 제공한다.

그것을 알기 위해서는 3가지의 용어를 알아야한다.

  • SimpAnnotationMethod : @MessageMapping과 같은 어노테이션이다.
  • SimpleBroker : client의 정보를 메모리 상에 들고 있고, client로 메세지를 내보낸다.
  • Channel : 3가지 종류의 Channel이 존재한다.
    • clientInBoundChannel : Client에서 서버로 들어오는 요청을 전달하는 채널
    • clientOutBoundChannel : 서버에서 Client로 메세지를 내보내는 채널
    • brokerChannel : Server 내부에서 사용하는 채널

우리는 인터셉터를 구현하여 채널을 가로채서 필요한 검증을 할 수 있다.

커스텀을 시작하자.

InBoundChannel을 가로채서 JWT 검증을 하자.

SocketConfig 수정

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class SocketConfig implements WebSocketMessageBrokerConfigurer {
		
    private final ChatPreHandler chatPreHandler;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/api/websocket").setAllowedOriginPatterns("*").withSockJS();

    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/queue", "/topic");

        registry.setApplicationDestinationPrefixes("/api");

    }
		
		// 여기 아래 부분 코드 추가
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(chatPreHandler);
    }

}

인터셉터를 추가해주었다.

인터셉터 구현

PreHandler.java 생성

@RequiredArgsConstructor
@Component
public class ChatPreHandler implements ChannelInterceptor {

    private final JwtService<User> jwtService;
    private final JwtProperties jwtProperties;
    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);

        // 헤더 토큰 얻기
        String authorizationHeader = String.valueOf(headerAccessor.getNativeHeader("Authorization"));
        // 토큰 자르기 fixme 토큰 자르는 로직 validate 로 리팩토링

        if(authorizationHeader == null || authorizationHeader.equals("null")){
            throw new MessageDeliveryException("메세지 예외");
        }

        String token = authorizationHeader.substring(BEARER_PREFIX.length());

        // 토큰 인증
        Claims claims;
        try{
            claims = jwtService.verifyToken(token, jwtProperties.getAccessTokenSigningKey());
        }catch (MessageDeliveryException e){
            throw new MessageDeliveryException("메세지 에러");
        }catch (MalformedJwtException e){
            throw new MessageDeliveryException("예외3");
        }

        return message;

    }
}

해당 인터셉터는 메세지가 서버에 도착해서 발행이 되기 전에 동작하는 인터셉터이다.

  1. 메세지 헤더에 존재하는 “Authorization”을 받는다.
  2. 받은 값을 검증한다.
  3. 만료나 변조 시, 예외를 터트린다.

그럼 이제 예외를 터트린 상황을 구현해야한다. 어떻게 구현할까?

ErrorHandler 구현

이 또한, Spring에서 제공해준다.

소켓에서 예외가 터졌을 경우를 위하여 Handler를 등록할 수 있게 제공한다.

SocketConfig 수정

@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/api/websocket").setAllowedOriginPatterns("*").withSockJS();
				//아래 코드 추가
        registry.setErrorHandler(chatErrorHandler);

    }

소켓 통신 중, 예외가 발생했을 때 “chatErrorHandler”로 제어권이 넘어간다.

ErrorHandler.java 생성


@Component
public class ChatErrorHandler extends StompSubProtocolErrorHandler {

    public ChatErrorHandler() {
        super();
    }

    @Override
    public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
    {
        Throwable exception = new MessageDeliveryException("abc");
        if (exception instanceof MessageDeliveryException)
        {

            return handleUnauthorizedException(clientMessage, exception);
        }

        return super.handleClientMessageProcessingError(clientMessage, ex);
    }

    private Message<byte[]> handleUnauthorizedException(Message<byte[]> clientMessage, Throwable ex)
    {

        ApiError apiError = new ApiError(
                ex.getMessage());

        return prepareErrorMessage(clientMessage, apiError, String.valueOf(ErrorCodeConstants.UNAUTHORIZED_STRING));

    }

    private Message<byte[]> prepareErrorMessage(Message<byte[]> clientMessage, ApiError apiError, String errorCode)
    {

        String message = apiError.getErrorMessage();

        StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);

        accessor.setMessage(errorCode);
        accessor.setLeaveMutable(true);

        return MessageBuilder.createMessage(message.getBytes(StandardCharsets.UTF_8), accessor.getMessageHeaders());
    }
}

예외가 발생했을 시,

  1. “handleClientMessageProcessingError” 실행
  2. 예외 종류에 따른 메소드 실행

코드가 완벽하지 않지만, 임의로 동작하도록 구현했다. (추 후에 예외 종류에 따른 로직을 분기하자.)

이렇게 구현했을 경우 JWT에러가 터지면 어떻게 동작하는지 살펴보자.

  • ws 통신을 보기위해서는 network → ws 에서 볼 수 있다.

에러에 따른 메세지가 소켓으로 응답 되는 것을 볼 수 있다.

해당 로직을 구현하면서 알아낸 것을 살펴보자.

추가 내용

ErrorHandler에서 Message Command 내용을 바꾸면 다르게 동작한다.

// 에러 커맨드
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);

// 메세지 커맨드
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.MESSAGE);

// 발행 커맨드
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.SEND);

커맨드들이 많이 존재한다.

STOMP는 커맨드에 따라 다르게 로직이 수행된다.

  • 클라이언트의 커맨드
    • DISCONNECT : 소켓을 끊는다.
    • CONNECT : 소켓을 연결한다.
    • SUBSCRIBE : Destination을 구독한다.
    • UNSUBSCRIBE : Destination을 구독 취소한다.
    • SEND : 클라이언트가 메세지를 보낸다.
    • ACK : HandShake 과정
    • NACK : HandShake 과정
    • BEGIN : -
    • COMMIT : -
    • ABORT : -
  • 서버의 커맨드
    • CONNECTED : 서버와 소켓 연결되었다.
    • RECEIPT : -
    • MESSAGE : 메세지이다.
    • ERROR : 에러이다.

몇개는 잘 모르겠다.. 레퍼런스를 찾아보았으나 포기.

어쨌든 커맨드에 따라 STOMP의 일이 달라진다.

(ex . SEND일 경우 PreHandler를 새롭게 타게 된다.)


예외 캐치하기

상황에 따라 다른 예외가 던져진다. 그리고 예외에 따른 Error 응답이 달라진다.

응답을 다르게 하기 위해서는 각 종류의 예외를 체크할 수 있어야 한다.

먼저 어떻게 예외가 던져지는지 살펴보자.

ErrorHandler 수정

@Override
    public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
    {
        Throwable exception = ex;

        if(exception instanceof MalformedInputException){
            log.info("멀폼 익셉션");
            return handleUnauthorizedException(clientMessage, exception);
        }

        if(exception instanceof JwtExpiredTokenException){
            log.info("만료 익셉션");
            return handleUnauthorizedException(clientMessage, exception);

        }
        if (exception instanceof MessageDeliveryException)
        {
						log.info("예외 내용 = {}", exception.getMessage());
            log.info("예외 내용의 내용 = {}", exception.getCause().getMessage());
  
            
            return handleUnauthorizedException(clientMessage, exception);
        }

        return super.handleClientMessageProcessingError(clientMessage, ex);
    }

log를 찍어보면 다음과 같다.

중요한 사실을 알 수 있다.

MessageDeliveryException 속에 MalformedJwtException이 포함되어 있다.

무조건 MessageDeliveryException이 터진다.

우리는 속에 포함되어있는 실제 Exception을 캐치해야한다.

근데 Exception이 Object로 구현되어 있어서인지.. instance로 캐치하기는 힘들다. 그래서 지정해준 메세지로 등록을 해주어야 할 듯 하다.

ChatErrorHandler 수정

@Override
    public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
    {
        if(ex.getCause().getMessage().equals("JWT")){
            return handleJwtException(clientMessage, ex);
        }

        if(ex.getCause().getMessage().equals("Auth")){
            return handleUnauthorizedException(clientMessage, ex);
        }

        return super.handleClientMessageProcessingError(clientMessage, ex);
    }

Prehandler 수정

@RequiredArgsConstructor
@Component
public class ChatPreHandler implements ChannelInterceptor {

    private final JwtService<User> jwtService;
    private final JwtProperties jwtProperties;
    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);

        // 헤더 토큰 얻기
        String authorizationHeader = String.valueOf(headerAccessor.getNativeHeader("Authorization"));
        String command = String.valueOf(headerAccessor.getHeader("stompCommand"));
        // 토큰 자르기 fixme 토큰 자르는 로직 validate 로 리팩토링
        if(!command.equals("SEND")){

            return message;
        }
        if(authorizationHeader == null || authorizationHeader.equals("null")){

            throw new MalformedJwtException("JWT");
        }

        String token = authorizationHeader.substring(BEARER_PREFIX.length());
        
        // 토큰 인증
        Claims claims;
        try{
            claims = jwtService.verifyToken(token, jwtProperties.getAccessTokenSigningKey());
        }catch (JwtExpiredTokenException e){
            throw new MessageDeliveryException("JWT");
        }catch (MalformedJwtException e){
            throw new MalformedJwtException("JWT");
        }catch (JwtModulatedTokenException e){
            throw new JwtModulatedTokenException("JWT");
        }
        
        // Principal로 담을 예정
        User verifiedUser = jwtService.convertUserModel(claims);

        return message;

    }
}

이제 예외 상황을 캐치할 수 있다. 이제 캐치한 예외에 따른 Message를 생성해주자.


Message 커스텀하기.

그럼 이제, Client에게 줄 Error를 커스텀해보자.

Message 객체

메세지 인터페이스는 다음과 같다.

public interface Message<T> {

	/**
	 * Return the message payload.
	 */
	T getPayload();

	/**
	 * Return message headers for the message (never {@code null} but may be empty).
	 */
	MessageHeaders getHeaders();

}

메세지 객체 구현 객체는 2가지가 있다.

  • ErrorMessage : 에러 메세지
  • GenericMessage : 그 외 일반 메세지

ErrorMessage

ErrorMessage를 살펴보자.

public class ErrorMessage extends GenericMessage<Throwable> {

	private static final long serialVersionUID = -5470210965279837728L;

	@Nullable
	private final Message<?> originalMessage;

	/**
	 * Create a new message with the given payload.
	 * @param payload the message payload (never {@code null})
	 */
	public ErrorMessage(Throwable payload) {
		super(payload);
		this.originalMessage = null;
	}

	/**
	 * Create a new message with the given payload and headers.
	 * The content of the given header map is copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers to use for initialization
	 */
	public ErrorMessage(Throwable payload, Map<String, Object> headers) {
		super(payload, headers);
		this.originalMessage = null;
	}

	/**
	 * A constructor with the {@link MessageHeaders} instance to use.
	 * <p><strong>Note:</strong> the given {@code MessageHeaders} instance
	 * is used directly in the new message, i.e. it is not copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers
	 */
	public ErrorMessage(Throwable payload, MessageHeaders headers) {
		super(payload, headers);
		this.originalMessage = null;
	}

	/**
	 * Create a new message with the given payload and original message.
	 * @param payload the message payload (never {@code null})
	 * @param originalMessage the original message (if present) at the point
	 * in the stack where the ErrorMessage was created
	 * @since 5.0
	 */
	public ErrorMessage(Throwable payload, Message<?> originalMessage) {
		super(payload);
		this.originalMessage = originalMessage;
	}

	/**
	 * Create a new message with the given payload, headers and original message.
	 * The content of the given header map is copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers to use for initialization
	 * @param originalMessage the original message (if present) at the point
	 * in the stack where the ErrorMessage was created
	 * @since 5.0
	 */
	public ErrorMessage(Throwable payload, Map<String, Object> headers, Message<?> originalMessage) {
		super(payload, headers);
		this.originalMessage = originalMessage;
	}

	/**
	 * Create a new message with the payload, {@link MessageHeaders} and original message.
	 * <p><strong>Note:</strong> the given {@code MessageHeaders} instance
	 * is used directly in the new message, i.e. it is not copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers
	 * @param originalMessage the original message (if present) at the point
	 * in the stack where the ErrorMessage was created
	 * @since 5.0
	 */
	public ErrorMessage(Throwable payload, MessageHeaders headers, Message<?> originalMessage) {
		super(payload, headers);
		this.originalMessage = originalMessage;
	}

	/**
	 * Return the original message (if available) at the point in the stack
	 * where the ErrorMessage was created.
	 * @since 5.0
	 */
	@Nullable
	public Message<?> getOriginalMessage() {
		return this.originalMessage;
	}

	@Override
	public String toString() {
		if (this.originalMessage == null) {
			return super.toString();
		}
		return super.toString() + " for original " + this.originalMessage;
	}

}

많을 것을 볼 필요는 없고.. “GenericMessage”를 상속받은 것을 알 수 있다.

그럼 GenericMessage를 살펴보자.

GenericMessage

public class GenericMessage<T> implements Message<T>, Serializable {

	private static final long serialVersionUID = 4268801052358035098L;

	private final T payload;

	private final MessageHeaders headers;

	/**
	 * Create a new message with the given payload.
	 * @param payload the message payload (never {@code null})
	 */
	public GenericMessage(T payload) {
		this(payload, new MessageHeaders(null));
	}

	/**
	 * Create a new message with the given payload and headers.
	 * The content of the given header map is copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers to use for initialization
	 */
	public GenericMessage(T payload, Map<String, Object> headers) {
		this(payload, new MessageHeaders(headers));
	}

	/**
	 * A constructor with the {@link MessageHeaders} instance to use.
	 * <p><strong>Note:</strong> the given {@code MessageHeaders} instance is used
	 * directly in the new message, i.e. it is not copied.
	 * @param payload the message payload (never {@code null})
	 * @param headers message headers
	 */
	public GenericMessage(T payload, MessageHeaders headers) {
		Assert.notNull(payload, "Payload must not be null");
		Assert.notNull(headers, "MessageHeaders must not be null");
		this.payload = payload;
		this.headers = headers;
	}

	@Override
	public T getPayload() {
		return this.payload;
	}

	@Override
	public MessageHeaders getHeaders() {
		return this.headers;
	}

	@Override
	public boolean equals(@Nullable Object other) {
		if (this == other) {
			return true;
		}
		if (!(other instanceof GenericMessage)) {
			return false;
		}
		GenericMessage<?> otherMsg = (GenericMessage<?>) other;
		// Using nullSafeEquals for proper array equals comparisons
		return (ObjectUtils.nullSafeEquals(this.payload, otherMsg.payload) && this.headers.equals(otherMsg.headers));
	}

	@Override
	public int hashCode() {
		// Using nullSafeHashCode for proper array hashCode handling
		return (ObjectUtils.nullSafeHashCode(this.payload) * 23 + this.headers.hashCode());
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(getClass().getSimpleName());
		sb.append(" [payload=");
		if (this.payload instanceof byte[]) {
			sb.append("byte[").append(((byte[]) this.payload).length).append(']');
		}
		else {
			sb.append(this.payload);
		}
		sb.append(", headers=").append(this.headers).append(']');
		return sb.toString();
	}

}

2가지의 변수가 존재한다.

  • payload : 실제 데이터
  • headers : 말그대로 해더

이것을 보면 우리는 payload와 header를 커스텀하면 되는 것을 알 수 있다.

그럼 커스텀 해보자!

커스텀 내용

먼저 어떻게 예외 상황을 응답할지 부터 보자.

AccessToken 만료 및 변조 예외

ERROR
message:{
		status: {
			code : 1302
			message : "인증 토큰이 만료되었습니다."
		}
}
content-length:~

(body 없음)
0000

그 외 예외(헤더 부적합, 헤더 요소 부족, 기타 예외)

ERROR
message:{
		status: {
			code : 1303
			message : "기타 오류입니다."
		}
}
content-length:~

(body 없음)
0000

MessageBuilder

메세지를 생성하기 위해서는 “MessageBuilder”를 사용하는 것이 편리하다.

해당 클래스에서 “createMessage” 메소드가 static으로 선언되어 있다. 이 메소드를 이용하자.

MessageBuilder.createMessage(code.getBytes(StandardCharsets.UTF_8), accessor.getMessageHeaders())

위의 메소드를 이용한 전체 코드를 보자!

@Component
public class ChatErrorHandler extends StompSubProtocolErrorHandler {

    public ChatErrorHandler() {
        super();
    }

    @Override
    public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
    {
        if(ex.getCause().getMessage().equals("JWT")){
            return handleJwtException(clientMessage, ex);
        }

        if(ex.getCause().getMessage().equals("Auth")){
            return handleUnauthorizedException(clientMessage, ex);
        }

        return super.handleClientMessageProcessingError(clientMessage, ex);
    }

    // 권한 예외(해당 방에 접속하지 않았을 시)
    private Message<byte[]> handleUnauthorizedException(Message<byte[]> clientMessage, Throwable ex)
    {

        ApiError apiError = new ApiError(
                ex.getMessage());

        return prepareErrorMessage(RoomCode.NOT_PARTICIPATE_ROOM);

    }

    // JWT 예외
    private Message<byte[]> handleJwtException(Message<byte[]> clientMessage, Throwable ex){

        return prepareErrorMessage(JwtErrorCode.ACCESS_TOKEN_EXPIRATION);
    }

    // 메세지 생성
    private Message<byte[]> prepareErrorMessage(ResponseCode responseCode)
    {

        String code = String.valueOf(responseCode.getMessage());

        StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);

        accessor.setMessage(String.valueOf(responseCode.getCode()));
        accessor.setLeaveMutable(true);

        return MessageBuilder.createMessage(code.getBytes(StandardCharsets.UTF_8), accessor.getMessageHeaders());
    }

}

이렇게 구현하면 예외 응답으로 다음과 같이 온다!

완료.


To Do

  • Subscribe 요청 거절
  • 리팩토링

Reference

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

1개의 댓글

comment-user-thumbnail
2023년 6월 22일

너무너무 고마워요!!!

답글 달기