https://github.com/Seungkyu-Han/Toge-do-backend
GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다
Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.
github.com
프론트 팀원에게 에러가 발생한다고 연락이 왔다.
유효하지 않은 토큰으로 채팅을 보내면 에러가 발생한다는 것이었다.
물론 당연한 말이라고 생각이 되지만, 발생하는 에러가 500 에러이기에 해당 에러는 수정을 해야했다.
이 부분을 수정하려고 하니 현재 웹소켓도 문제가 있다고 생각되어, 이번 기회에 교체해보려고 한다.
현재 서버는
이렇게 연결을 한 후에 토큰을 전송하면서 인증을 하고 사용자 정보를 추출하고 있다.
@MessageMapping("/{groupId}")
fun publishChatMessage(
@Parameter(hidden = true) @Header(HttpHeaders.AUTHORIZATION) accessToken: String,
@DestinationVariable groupId: String,
@RequestBody messageReqDto: MessageReqDto
): Mono<ResponseEntity<Void>> {
return chatService.publishMessage(
groupId = groupId,
userId = jwtTokenProvider.getUserId(
headerUtil.extractAccessTokenFromHeader(accessToken)
)!!,
message = messageReqDto.message
).then(Mono.fromCallable { ResponseEntity(HttpStatus.OK) })
}
빠르게 개발하기 위해 이렇게 만들었었는데, 사실 어차피 연결이 완료되면 더 이상 인증할 필요가 없기 때문에 그냥 connect 할 때, 한 번만 인증을 하고 해당 세션을 유지하는 것이 맞을 것이다.
그렇기 때문에 이렇게 교체를 해보려고 한다.
우선 당연하게 Jwt 토큰을 추출할 component가 존재해야 한다.
우선 Stomp Config에 configureClientInboundChannel을 오버라이드 받아, interceptor에 새로운 필터를 작성한다.
@Configuration
@EnableWebSocketMessageBroker
class ChatConfig(
private val stompErrorHandler: StompErrorHandler,
private val filterChannelInterceptor: FilterChannelInterceptor
): WebSocketMessageBrokerConfigurer {
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(filterChannelInterceptor)
}
}
이제 여기에 들어가는 FilterChannelInterceptor가 연결을 시작하는 중간에 검증을 하는 역할을 한다.
@Component
class FilterChannelInterceptor(
private val headerUtil: HeaderUtil = HeaderUtil(),
private val jwtTokenProvider: JwtTokenProvider
): ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
val stompHeaderAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (stompHeaderAccessor!!.command == StompCommand.CONNECT) {
try{
val bearerToken = stompHeaderAccessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION)
val token = headerUtil.extractAccessTokenFromHeader(bearerToken!!)
val userId = jwtTokenProvider.getUserId(token)
stompHeaderAccessor.sessionAttributes?.put("userId", userId)
}catch(e: Exception){
throw AuthException()
}
}
return message
}
}
작성한 filter이다.
우선 메시지로부터 StompHeaderAccessor를 가져온다.
그리고 조건문을 이용해 StompCommand의 CONNECT, ERROR, MESSAGE 등등 중에서 연결을 시작할 때인 CONNECT 일 때만 동작하도록 한다.
Stomp로 연결을 하더라도, Header를 작성할 수가 있기 때문에 헤더에서 JwtToken을 가져와서 사용자 아이디를 추출한다.
그 다음에 StompHeaderAccesor의 세션에 넣어주는데, 이 때 그냥 유저의 아이디만을 넣어주는 것이 아닌 Authentication을 넣어줄 수도 있다.
나는 현재 프로젝트에서 Authentication이 구현되어 있지 않기 때문에 그냥 userId로 정보만 넣어줬다.
그리고 토큰에서 사용자 아이디를 추출하는 과정에서 생기는 모든 에러는 인증 에러로 생각하여, 모든 에러를 AuthException으로 바꾸어 throw해주었다.
일단 이렇게하면 연결 과정에서 토큰을 사용하여 인증을 수행하고, 실패한다면 연결을 종료할 수 있다.
하지만 사용자에게 인증이 실패했다는 내용은 알려줄 수 없게 된다.
그래서 이제 StompErrorHandler를 작성해야 한다.
다시 Stomp Config로 가서, 웹소켓에서 발생하는 모든 에러를 처리할 error handler를 추가해준다.
@Configuration
@EnableWebSocketMessageBroker
class ChatConfig(
private val stompErrorHandler: StompErrorHandler,
private val filterChannelInterceptor: FilterChannelInterceptor
): WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.setErrorHandler(stompErrorHandler)
}
}
이렇게 추가를 하고, ErrorHandler를 작성해보자.
@Configuration
class StompErrorHandler(
private val objectMapper: ObjectMapper
): StompSubProtocolErrorHandler(){
override fun handleClientMessageProcessingError(
clientMessage: Message<ByteArray>?,
ex: Throwable
): Message<ByteArray>? {
if (ex is MessageDeliveryException && ex.cause is AuthException){
val webSocketEventEnum = WebSocketEventEnum.AUTHORIZATION_ERROR
val stompHeaderAccessor = StompHeaderAccessor.create(StompCommand.ERROR)
stompHeaderAccessor.message = "Fail to send message"
val errorBody = objectMapper.writeValueAsString(
WebSocketEventDto(state = webSocketEventEnum.value, message = webSocketEventEnum.message)
).toByteArray(StandardCharsets.UTF_8)
return MessageBuilder.createMessage(errorBody, stompHeaderAccessor.messageHeaders)
}
return super.handleClientMessageProcessingError(clientMessage, ex)
}
}
우선 이때 발생하는 모든 에러는 MessageDeliveryException으로 온다.
그렇기에 에러가 발생하는 이유를 찾기 위해서는 MessageDeliveryException의 cause를 확인해야 한다.
나는 인증에서 생기는 모든 에러를 AuthException으로 처리했기 때문에, 해당 에러인지를 확인하고 그 때 에러메시지를 return하도록 했다.
내용을 message, body에 작성할 수 있는데, 나는 에러의 내용을 body에 적어주었다.
작성할 내용을 byteArray로 작성하여 넣어주면 된다.
이렇게 추가를 해두고, 만료된 토큰을 사용해보니
인증에 실패하면 클라이언트에게 에러메시지가 가는 것을 볼 수 있었다.
'토이 프로젝트' 카테고리의 다른 글
트랜잭션 격리 수준(Transaction Isolation Level) (0) | 2025.03.26 |
---|---|
메모리 단편화, 객체 생성 시간을 줄이기 위한 ObjectPool 적용 (1) | 2025.02.11 |
Hexagonal Architecture 적용하기 (1) | 2025.01.22 |
직렬화 과정에서 함수 조건 검사 (0) | 2025.01.13 |
Spring Webflux + Stomp를 사용해 채팅 구현하기 (0) | 2025.01.05 |