์น์์ผ ์ฐ๊ฒฐ JWT ์ธ์ฆ ํธ๋ฌ๋ธ ์ํ
๐์ ๋ฆฌ
- 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์์ ์ฟ ํค๋ก ์ ๋ฌ๋ ํ ํฐ์ ๊บผ๋ด ๊ฒ์ฆ์ ์งํํ์ต๋๋ค.