WebSocket + JWT
관련 글
- Websocket
- Websocket + 부가기능
- Websocket (채팅 기록 json 파일 저장하기)
- Sse
- Sse 문제점
- Websocket + jwt 👈🏻
- Websocket test
- Jmh - 채팅 파일 refactoring
내가 채팅방을 작성하면서 security를 구현한 이유는 stomp를 사용하면 헤더에 token을 추가해 보안을 강화할 수 있다는 것
이제 코드로 작성해본다.
WebSocketConfig
StompHandler가 Websocket 앞단에서 token을 체크할 수 있도록 interceptor로 설정한다.
1
2
3
4
5
6
7
private final StompHandler stompHandler;
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
// jwt 토큰 검증을 위해 생성한 stompHandler를 인터셉터로 지정해준다.
registration.interceptors(stompHandler);
}
JwtTokenProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 기존에 작성한 코드 활용
// jwt token을 복호화 하여 이름을 얻는다.
public String getUsername(String token) {
log.info("[getUsername] 토큰 기반 회원 구별 정보 추출");
// 토큰을 생성할때 넣었던 sub 값 추출
String info = getClaims(token).getBody().getSubject();
log.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료");
return info;
}
private Jws<Claims> getClaims(String jwt){
try{
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt);
}catch (SignatureException e){ // 잘못된 jwt signature
log.error("Invalid JWT signature");
throw e;
} catch (MalformedJwtException e){
log.error("Invalid JWT token");
throw e;
} catch (ExpiredJwtException e){ // jwt 만료
log.error("Expired JWT token");
throw e;
} catch (UnsupportedJwtException e){ // 지원하지 않는 jwt
log.error("Unsupported JWT token");
throw e;
} catch (IllegalArgumentException e){ // 잘못된 jwt 토큰
log.error("JWT claims string is empty");
throw e;
}
}
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt)
을 사용하는 코드가 많아서 묶어서 사용
예시 출력
getClaims(token)
: header={alg=HS256},body={sub=haedal, roles=USER, iat=1516239022, exp=1234567890},signature=ajslkdfjaldkfjaDSLFieo
정리글 👉🏻 Session과 jwt
StompHandler
websocket 연결 시 요청 header의 jwt token 유효성을 검증하는 코드를 추가한다.
유효하지 않은 jwt 토큰이 세팅될 경우 websocket 연결을 하지 않고 예외처리 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequiredArgsConstructor
@Component
public class StompHandler implements ChannelInterceptor {
private final JwtTokenProvider jwtTokenProvider;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
// websocket 연결 시 header의 jwt token 검증
if (StompCommand.CONNECT.equals(headerAccessor.getCommand())) {
jwtTokenProvider.validateToken(headerAccessor.getFirstNativeHeader("Authorization"));
}
return message;
}
}
headerAccessor.getNativeHeader()
: 지정된 네이티브 헤더가 있는 경우 모든 값을 반환
headerAccessor.getFirstNativeHeader()
: 지정된 네이티브 헤더가 있는 경우 첫 번째 값을 반환
preSend() 메소드에서 클라이언트가 CONNECT할 때 헤더로 보낸 Authorization에 담긴 jwt Token을 검증하도록 한다.
1
2
3
4
5
6
function connect(event) {
let socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({Authorization:token}, onConnected, onError);
}
ChatController
1
2
3
4
5
6
7
8
9
10
11
12
function sendMessage(event) {
let messageContent = messageInput.value.trim();
if (messageContent && stompClient) {
let chatMessage = {
roomId: roomId, sender: username, message: messageInput.value, type: 'TALK'
};
saveFile(chatMessage)
stompClient.send("/app/chat/sendMessage", {Authorization:token}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault(); // 계속 바뀌는 것을 방지함
}
Authorization으로 header에 token값을 넣었으니 Controller에서 @Header("Authorization")
라고 작성했다.
1
2
3
4
5
6
7
8
9
10
11
@Controller
@RequiredArgsConstructor
public class ChatController {
private final JwtTokenProvider jwtTokenProvider;
@MessageMapping("/chat/sendMessage")
public void sendMessage(@Payload ChatMessage chatMessage, @Header("Authorization") String token) {
String nickname = jwtTokenProvider.getUsername(token);
template.convertAndSend("/topic/public/" + chatMessage.getRoomId(), chatMessage);
}
}
websocket을 통해 서버에 메세지가 send 되었을 때도 jwt token 유효성 검증이 필요하다.
위와 같이 회원 대화명(id)를 조회하는 코드를 삽입하여 유효성이 체크될 수 있도록 한다.
reference
[Spring boot + React] STOMP로 실시간 채팅 구현하기 (3) - 사용자 인증 구현하기
Spring Boot + STOMP + JWT Socket 인증하기
Spring websocket chatting server(4) - SpringSecurity+Jwt를 적용하여 보완강화하