์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

์›น์†Œ์ผ“ ์—ฐ๊ฒฐ JWT ์ธ์ฆ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

Seung__Yong 2024. 2. 15. 14:40

๐Ÿ˜์ •๋ฆฌ

- Websocket Handshake์š”์ฒญ์€ ์ผ๋ฐ˜์ ์ธ HTTP์š”์ฒญ์„ ์žก๋Š” HandlerInterceptor์— ๊ฑธ๋ฆฌ์ง€ ์•Š์•„ ๋ณ„๋„์˜ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

- STOMP CLIENT์—์„œ ๋‹ด๋Š” ํ—ค๋”๋“ค์€ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์ดํ›„ STOMP ์—ฐ๊ฒฐ์ด ์ง„ํ–‰๋  ๋•Œ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

 ์ฆ‰, Handshake Interceptor์—์„œ ํ† ํฐ์„ ๋ฐ›์•„ ์ธ์ฆ์„ ์ง„ํ–‰ํ•  ์ˆ˜๊ฐ€ ์—†์Œ

 

- ChannelInterceptor ์ด์šฉํ•˜์ž -> COMMAND๊ฐ€ CONNECT์ผ ๋•Œ๋งŒ ์ธ์ฆ ์—ฌ๋ถ€ ๊ฒ€์ฆํ•˜๋„๋ก ๊ตฌํ˜„ 

- ์œ„ ๋ฐฉ๋ฒ•์€ ์ธ์ฆ์„ ์—ฌ๋Ÿฌ๋ฒˆ ์ง„ํ–‰ํ•˜์ง€๋Š” ์•Š์ง€๋งŒ ๋ฉ”์‹œ์ง€ ์ „์†ก์‹œ์—๋„ ํ† ํฐ์„ ๋‹ฌ๊ณ  ๋‹ค๋…€์•ผ ํ•ด์„œ ์ „์†ก ๋น„์šฉ์ด ์ฆ๊ฐ€ํ•จ
- ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…์€ ํ† ํฐ์„ ์ฟ ํ‚ค๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์‹œ ๋™์ž‘ํ•˜๋Š” Handshake Interceptor์—์„œ ํ•œ๋ฒˆ๋งŒ ๊ฒ€์‚ฌํ•˜๋„๋ก ํ•œ๋‹ค.  

 

๐Ÿ˜์›น์†Œ์ผ“ ์ด์šฉ์‹œ ์ธ์ฆ์ด ์•ˆ๋˜๋Š” ๋ฌธ์ œ 

์ฑ„ํŒ… ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„์€ ํ•ด๋ดค์ง€๋งŒ ์ฑ„ํŒ…์— ์ธ์ฆ์„ ์ ์šฉํ•ด๋ณธ์ ์€ ์—†์—ˆ๊ธฐ์— ๋‹จ์ˆœํ•˜๊ฒŒ ์ƒ๊ฐํ•˜์—ฌ ์•„๋ž˜์ฒ˜๋Ÿผ ๊ตฌํ˜„์„ ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

HandlerInterceptor๋ฅผ ์ด์šฉํ•ด ๊ฒ€์ฆ ๋ฐ ์ธ์ฆ๊ฐ์ฒด ์„ค์ •์„ ์ง„ํ–‰ํ•˜๊ณ  LoginChecking์—์„œ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. 

    @LoginChecking  // ์ปค์Šคํ…€ anntation์— AOP์ ์šฉ
    @MessageMapping("/chat/message")
    public void sendMessage(ChatMessageRequest chatMessageRequest) {
        ChatResponse chatResponse = chatService.create(chatMessageRequest);
        messagingTemplate.convertAndSend("/sub/chat-room/" + chatMessageRequest.getChatRoomId(), chatResponse);
    }

 

๋‹น์—ฐํžˆ ์•ˆ๋˜๊ฒ ์ฃ ..??ใ…Žใ…Žใ…Ž ์ด์œ ๋Š” ๋ชฐ๋ž๊ธฐ์— ๊ณผ์ •์„ ์„ค๋ช…ํ•˜์ž๋ฉด ๋””๋ฒ„๊น…์„ ์ฐ์–ด๋ดค์„ ๋•Œ ํ† ํฐ์„ ๋„ฃ์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ธ์ฆ ์˜ˆ์™ธ๊ฐ€ ๊ณ„์† ํ„ฐ์ง€๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

HandlerInterceptor๋Š” HTTP์š”์ฒญ์— ๋Œ€ํ•œ ์ธํ„ฐ์…‰ํ„ฐ์ธ๋ฐ ์œ„ ์š”์ฒญ์€ ์›น์†Œ์ผ“ ์š”์ฒญ์ด๋ผ ๊ฐ€๋กœ์ฑŒ ์ˆ˜๊ฐ€ ์—†์—ˆ๋˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ž„์‹œ ๋ฐฉํŽธ
๋กœ๊ทธ์ธ ์ฒดํ‚น์„ ์ œ๊ฑฐํ•˜๊ณ  ์ง์ ‘ ํ† ํฐ์„ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค.

-> ๋ฌธ์ œ์ ์ด ๋งค๋ฒˆ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ฒŒ ๋˜๊ฒ ์ฃ ??

@MessageMapping("/chat/message")
public void sendMessage(ChatMessageRequest chatMessageRequest, @Header("Authorization") String accessToken) {
    String ac = JwtUtils.extractBearerToken(accessToken);

    if (ac != null) {
        if (jwtProvider.validateAccessToken(ac)) {
            Authentication authentication = jwtProvider.getAuthenticationByAccessToken(ac);
        }
    }

    ChatResponse chatResponse = chatService.create(chatMessageRequest);
    messagingTemplate.convertAndSend("/sub/chat-room/" + chatMessageRequest.getChatRoomId(), chatResponse);
}

 

๐Ÿ˜์—ฐ๊ฒฐ ๊ณผ์ •์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ?

๋งค๋ฒˆ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋„ˆ๋ฌด ๋น„ํšจ์œจ์ ์ด๋ผ ๋А๊ผˆ๊ณ  ์—ฐ๊ฒฐ ๊ณผ์ •์—์„œ๋งŒ ๊ฒ€์ฆ์„ ํ•˜๋ฉด ๋˜์ง€์•Š์„๊นŒ? ๋ผ๋Š” ์ƒ๊ฐ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉ๋ฒ•์€ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ๊ณผ์ •์ธ Handshake๊ณผ์ •์—์„œ ๊ฒ€์ฆ์„ ํ•˜์ž! ์ด ์ƒ๊ฐ์— ์›น์†Œ์ผ“ ํ•ธ๋“œ์…ฐ์ดํ‚น ๊ณผ์ •์„ ์ธํ„ฐ์…‰ํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ์ฃ 
-> ์›น์†Œ์ผ“ ํ•ธ๋“œ์…ฐ์ดํฌ ์ด์ „์—๋Š” HTTP Header๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•˜์—ฌ ์ด ๋ถ€๋ถ„์—์„œ ๊ฒ€์ฆ์„ ํ•˜๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Component
@RequiredArgsConstructor
public class WebSocketInterceptor implements HandshakeInterceptor {
    private final JwtProvider jwtProvider;
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        HttpHeaders headers = request.getHeaders();
        String authToken = headers.getFirst(HttpHeaders.AUTHORIZATION);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

}
}

 

๐Ÿ˜์™œ Header๊ฐ€ NULL๊ฐ’์ด ๋œฐ๊นŒ

์ €๋Š” STOMP๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ๊ณ  ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑ๋ผ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ชฝ์—์„œ ํ† ํฐ์ด ๋‹ด๊ธด ๊ฒƒ์€ log๋กœ๋„ ํ™•์ธ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ํ™•์ธ์ด ์•ˆ๋˜๋Š”๊ฒŒ ํ™•์‹คํ–ˆ์ฃ 

try {
      console.log(">>>  ์ฒซ ์—ฐ๊ฒฐ ์‹œ๋„ ");

      const client = new Stomp.Client({
        webSocketFactory: () => socket,
        connectHeaders: {
          Authorization: `Bearer ${ACCES_TOKEN}`,
          name: `name`,
          password: `password`,
        },
        heartbeatIncoming: 0,
        heartbeatOutgoing: 0,
      });

..... ์ดํ•˜ ๋‚ด์šฉ ์ƒ๋žต .....

 

์ด์œ ๋Š” ์ œ๊ฐ€ ์œ„์—์„œ ๋‹ด์•„๋†“์•˜๋˜ ํ—ค๋”๋“ค์€ STOMP ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์š”์ฒญ ์‹œ ์ƒ์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
STOMP CLIENT๋ฅผ ์ด์šฉํ–ˆ์„ ๋•Œ ์—ฐ๊ฒฐ ๊ณผ์ •์ด ์•„๋ž˜์™€ ๊ฐ™์•˜๋˜๊ฑฐ์ฃ 

 

1. STOMP ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
2. ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ ์‹œ๋„
3. ์›น์†Œ์ผ“ ํ•ธ๋“œ์…ฐ์ดํฌ
4. STOMP ํ•ธ๋“œ์…ฐ์ดํฌ - ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ์ด ์„ค์ •๋œ ํ›„, STOMP ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์— STOMP ํ•ธ๋“œ์…ฐ์ดํฌ ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์ด ํ•ธ๋“œ์…ฐ์ดํฌ ์š”์ฒญ์—๋Š” STOMP ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ถ”๊ฐ€ ์ •๋ณด๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(์š” ์‹œ์ ๋ถ€ํ„ฐ ํ—ค๋”๋“ค ์ด์šฉ ๊ฐ€๋Šฅ)


5. ์„œ๋ฒ„ ์‘๋‹ต

 

๐Ÿ˜ChanelInterceptor๋ฅผ ์ด์šฉํ•˜์ž 

ChannelInterceptor ๋Š” ๋ฉ”์„ธ์ง• ์š”์ฒญ/์‘๋‹ต(InBound, OutBound) ์„ ๊ฐ€๋กœ์ฑ„ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ผ์ข…์˜ ํ•„ํ„ฐ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ๋Š” STOMP CLIENT์—์„œ ๋‹ด์€ connectHeaders๋ฅผ ์ด์šฉ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œ์— STOMP ๋ฉ”์‹œ์ง€์—๋Š” ๋ฉ”์‹œ์ง€์˜ ๋™์ž‘์„ ์ •์˜ํ•˜๋Š” Command(CONNECT, SEND, SUBSCRIBE, MESSAGE ๋“ฑ๋“ฑ) ๊ฐ€ ๋“ค์–ด์žˆ์ฃ  

 

์ฆ‰, ์•„๋ž˜์™€ ๊ฐ™์ด CONNECT ์‹œ์—๋งŒ ํ† ํฐ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ๋์Šต๋‹ˆ๋‹ค. 

 

@Component
@RequiredArgsConstructor
public class StompInterceptor implements ChannelInterceptor {
    private final JwtProvider jwtProvider;
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        if (StompCommand.CONNECT.equals(accessor.getCommand()))
            validateToken(accessor);

        return message;
    }

    private void validateToken(StompHeaderAccessor accessor) {
        String accessToken = JwtUtils.extractBearerToken(accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION));

        if (accessToken == null)
            throw new ForbiddenException(ErrorCode.NOT_LOGIN);

        jwtProvider.validateAccessToken(accessToken);

    }
}

 

๐Ÿ˜๋ฌธ์ œ์ 

๊ฒ€์ฆ์„ ๋งค๋ฒˆ ํ•˜์ง„ ์•Š์„ ์ˆ˜ ์žˆ๊ฒŒ๋์ง€๋งŒ ์‚ฌ์‹ค  ๋ฉ”์„ธ์ง€์— ํ† ํฐ์ด ํ•ญ์ƒ ๋‹ด๊ธฐ๊ธฐ ๋•Œ๋ฌธ์— ์ „์†ก๋น„์šฉ์€ ์ปค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” websocket์—ฐ๊ฒฐ์„ ์ปจํŠธ๋กคํ•  ์ˆ˜ ์—†์œผ๋‹ˆ ์—ฐ๊ฒฐ ์ „์— HTTP์š”์ฒญ์œผ๋กœ ์ธ์ฆ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•˜๋ ค๊ณ  ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. CORS๋ฅผ ๋ง‰์•„๋†“๋Š”๋‹ค๋ฉด ํ”„๋กœ์ ํŠธ์—์„œ ์˜๋„ํ•œ๋Œ€๋กœ๋งŒ ์›น์†Œ์ผ“์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์œ ์ผํ•œ ๊ฒฝ๋กœ๊ฐ€ ๋˜๋Š”๊ฑฐ์ฃ  ์ด ๋˜ํ•œ ๋ฌธ์ œ์ ์€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

 

Channel Interceptor ์ด์šฉ
์žฅ์  : ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ธฐ ์ง์ „์— ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์„ฑ์ด๋‚˜ ์ธ์ฆ ์™ธ์— ๊ฒ€์ฆ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์ข‹์Œ

๋‹จ์  : ๋ฉ”์‹œ์ง€์— ๋งค๋ฒˆ ํ† ํฐ์ด ๋‹ด๊ฒจ์„œ ์ „์†ก๋จ 

 

์—ฐ๊ฒฐ์ „ HTTP์š”์ฒญ์œผ๋กœ ์ธ์ฆ ์—ฌ๋ถ€ ํ™•์ธ(CORS์„ค์ •์œผ๋กœ ์ ‘๊ทผ ๊ฒฝ๋กœ๋ฅผ ์ฐจ๋‹จ)

์žฅ์  : ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ๋งค๋ฒˆ ํ† ํฐ์„ ๋‹ด์ง€ ์•Š์•„๋„๋จ
๋‹จ์  : ์ด๋ก ์ƒ ๋ง‰์•˜์ง€๋งŒ ํ™•์‹คํ•˜๊ฒŒ ๋ง‰ํžŒ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Œ + ๋””ํ…Œ์ผํ•œ ๊ฒ€์ฆ ๋ถˆ๊ฐ€๋Šฅ

 

๐Ÿ˜ํ•ด๊ฒฐ์ฑ…

Channel Interceptor๊นŒ์ง€ ์ธ์ฆ์„ ๋ฏธ๋ค˜๋˜ ์ด์œ ๋Š” ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ Handshake ์š”์ฒญ์ด STOMP CLIENT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ† ํฐ์„ ์ง์ ‘ ๋‹ด๊ธฐ ์–ด๋ ค์›Œ์„œ ์ž…๋‹ˆ๋‹ค.

์ฆ‰, ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ตณ์ด ์ˆ˜๋™์œผ๋กœ ๋‹ด์„ ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค. ์ €๋Š” HandshakeInterceptor์—์„œ ์ฟ ํ‚ค๋กœ ์ „๋‹ฌ๋œ ํ† ํฐ์„ ๊บผ๋‚ด ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.