Stomp interceptor로 보안을 강화하자

Alex·2024년 11월 29일
0

Plaything

목록 보기
39/118

Websockt을 사용할 때는 기본적인 Spring Boot의 토큰 기반 인증을 바로 사용할 수 없다.
Spring Boot가 HTTP 통신과 달리 WebSocket 통신의 헤더를 가로채서 토큰을 검증하지 못하기 때문이다.

별도의 인터셉터를 사용해서 JWT 토큰 인증을 해야 한다.

HandshakeInterceptor, ChannelInterceptor 이 두가지가 있는데 후자를 쓰면 된다.

HandshakeInterceptor에서 Connect, DisConnect 프레임을 가져와서 쓸 수 없기 때문이다.

(추가)

자바스크립트에서는 Websocket을 다룰 때 커스텀헤더를 추가하는 기능이 없다고 한다. 그래서, 처음 http로 연결요청을 할 때만 jwt 헤더로 추가가 가능하고, 그 이후에는 다른 방식으로 jwt 헤더를 파싱해야 하는듯하다.

짤막한 이론 공부

Every STOMP over WebSocket messaging session begins with an HTTP request. That can be a request to upgrade to WebSockets (that is, a WebSocket handshake) or, in the case of SockJS fallbacks, a series of SockJS HTTP transport requests.In short, a typical web application needs to do nothing beyond what it already does for security. The user is authenticated at the HTTP request level with a security context that is maintained through a cookie-based HTTP session (which is then associated with WebSocket or SockJS sessions created for that user) and results in a user header being stamped on every Message flowing through the application.(출처)

Stomp를 통한 웹소켓 사용도 결국은 Http 요청을 기반으로 시작하고, Http의 인증 방식에 의해서 인가과정을 거친다는 것이다.

Use the STOMP client to pass authentication headers at connect time.
Process the authentication headers with a ChannelInterceptor.
The next example uses server-side configuration to register a custom authentication interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT Message. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following example shows how to register a custom authentication interceptor:

Also, note that, when you use Spring Security’s authorization for messages, at present, you need to ensure that the authentication ChannelInterceptor config is ordered ahead of Spring Security’s. This is best done by declaring the custom interceptor in its own implementation of WebSocketMessageBrokerConfigurer that is marked with @Order(Ordered.HIGHEST_PRECEDENCE + 99).

커넥트 요청에 헤더에 토큰을 담아야 하고, 이걸 기반으로 같은 세션임을 확인한다.(그 이후로 토큰을 계속 보내니까)

스프링 시큐리티보다 ChannelInterceptor가 먼저 오도록 해야 한다고 한다.

Connect 요청은 JWT 검증이 필요하다.

Connect 요청을 할 땐 JWT 검증을 한다.

그리고, Subscribe를 할 때는 자신의 채널이 맞는지를 검증한다.

이 인터셉터를 Stomp에 등록해주면 끝이다.

구독에는 JWT 토큰을 보내지 않는다

구독을 할 때는 JWT 토큰을 보내지 않는 구조가 일반적인 거 같다.
위 코드로 구독할 때 계속 예외가 터졌다.
디버깅을 해보니 "Authorization" 헤더의 값이 null이었다.

웹소켓은 초기에 Connect를 하고 생긴 세션의 정보를 서버에 저장해서 관리하는 게 일반적으로 보인다.
이를 활용해서 세션을 식별할 수 있는 정보를 넣어두었다.

이렇게 send, disconnect, connect, subscribe 각각 검증하는 과정들을 추가해주었다.

메시지를 보낼 때마다 인증을 어떻게 할까?

위 같은 방식을 활용하기로 했다.
메시지를 보낼 때 JWT토큰에서 얻은 정보와 세션map에 저장해둔 유저의 정보가 일치해야 한다.

이건 메모리에 저장해둔 세션 map을 활용한다.

그런데, 우리 서비스는 매칭된 상대와만 채팅을 할 수 있다.
그렇다면, 지금 세션의 이용자가 메시지 수신인과 매칭된 상대인지를 검증하는 과정도 필요하다.

이를 위한 선택지가

1)DB조회

2)레디스 조회

3)메모리 조회

이렇게 세 가지가 있다.

1번은 우선 제외했다. 매번 채팅을 보낼 때마다 DB 조회를 하는 건 불필요한 리소스 낭비로 보인다.

2번이 고민되는 지점이었다.
레디스를 쓰면 DB를 쓸 때보다 더 나은 점이 뭘까?

우선 DB 부하를 줄일 수 있다. 채팅이 DB에 주는 부하가 상당할 것이기에
이것만으로도 괜찮은 메리트다.(그리고 이미 레디스를 사용하고 있기에)

그런데, 이 매칭 정보는 확인하자고 채팅 보낼 때마다 레디스에 요청을 보내는 게 부담스럽다.
네트워크를 타고서 요청을 보내야 하기 때문.
레디스 자체에 부하도 커질 것이다.

그럴바엔 차라리 메모리에 올려놓고 써도 되지 않을까?
매칭 정보가 없다면 DB에서 한번 조회하고
이를 로컬 메모리에 캐싱해두는 것이다.

그리고, 매일 새벽 5시에 매칭 정보는 캐시에서 지우도록 스케쥴링한다.
이용자 모두가 채팅을 매일 하지 않는 이상, 메모리에 계속 데이터를 두고 있을 필요가 없다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글