최근에 보완해서 글을 올렸어요~ 최근 글을 보시면 문제 해결이 더 빠를거에요!
https://shout-to-my-mae.tistory.com/430
웹소켓을 사용할 때 요청을 보내는 사람의 인증을 처리하는 방법은 여러 가지가 있다.
웹소켓은 HTTP 프로토콜을 업그레이드하여 연결을 시작하기 때문에, 연결 초기 단계에서 인증을 수행할 수 있는 방법을 활용할 수 있다.
1. 초기 HTTP 핸드셰이크에서 인증 토큰 사용
웹소켓 연결을 시작하기 전에, 클라이언트가 서버에 HTTP 요청을 보내는 초기 핸드셰이크 과정에서 인증 토큰(JWT 토큰 등)을 Authorization 헤더에 포함시키는 방법이다.
서버는 이 토큰을 검증하여 사용자를 인증한 후, 유효한 경우에만 웹소켓 연결을 수락한다.
const socket = new WebSocket('ws://example.com/ws', ['token', 'yourAuthTokenHere']);
2. 쿼리 파라미터를 통한 인증 토큰 전달
웹소켓 URL의 쿼리 파라미터에 인증 토큰을 포함하여 전달하는 방법도 있다. 이 방법은 구현이 간단하지만, URL에 토큰이 노출되므로 보안상의 리스크가 존재한다.
const socket = new WebSocket('ws://example.com/ws?token=yourAuthTokenHere');
3. 웹소켓 서브프로토콜을 사용한 인증
웹소켓 프로토콜은 여러 "서브프로토콜"을 지원할 수 있도록 설계되어 있다. 클라이언트가 연결 시도 시 인증 토큰을 서브프로토콜 중 하나로 전달하고, 서버에서는 이를 인증 메커니즘으로 사용할 수 있다.
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor()) // 서브 프로토콜 전달
.withSockJS();
}
// 클라이언트 사이드 예시
const socket = new WebSocket('ws://example.com/ws', 'yourAuthTokenHere');
4. 커넥션 열린 후의 첫 메시지로 인증
웹소켓 연결이 성공적으로 열린 직후, 클라이언트가 첫 번째 메시지로 인증 정보(예: 인증 토큰)를 서버에 전송하고, 서버는 이 메시지를 검증하여 연결을 유지하거나 끊을 수 있다. 이 방법은 연결 후 초기 통신 단계에서 추가적인 인증 로직을 처리해야 한다.
채택한 방법 : 1. 초기 HTTP 핸드셰이크에서 인증 토큰 사용
초기 웹소켓 연결시 JWT 토큰을 헤더에 추가하기
클라이언트
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
const headers = {
'Authorization': 'Bearer {token}'
};
stompClient.connect(headers, function(frame) {
// 연결 성공 시의 콜백
});
JWT를 헤더에 추가하기
서버
1. Spring Security 설정
websocket url인 /ws 은 Open url(인증X)로 설정해둔다. 초기 연결 요청시 웹소켓은 따로 인증 절차를 진행할 예정이기 때문이다.
2. 인터셉터 추가
Spring에서 JwtChannelInterceptor를 사용하여 STOMP 메시지가 처리되기 전에 헤더 내 JWT 토큰을 확인하고 검증할 수 있다.
StompCommand를 통해 웹소켓 연결, 메세지 전송, 종료 명령어에 대해 따로 작업을 지정할 수 있다.
/**
* WebSocket 채널에 JWT 검증하는 인터셉터
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtChannelInterceptor implements ChannelInterceptor {
private final MemberFindService userUtilityService;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
// 연결 요청시 JWT 검증
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Authorization 헤더 추출
List<String> authorization = accessor.getNativeHeader(JwtProvider.HEADER);
if (authorization != null && !authorization.isEmpty()) {
String jwt = authorization.get(0).substring(JwtProvider.TOKEN_PREFIX.length());
try {
// JWT 토큰 검증
DecodedJWT decodedJWT = JwtProvider.verify(jwt);
Long id = decodedJWT.getClaim("id").asLong();
// 사용자 정보 조회
Member member = userUtilityService.getUserById(id);
// 사용자 인증 정보 설정
CustomUserDetails userDetails = new CustomUserDetails(member);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JWTVerificationException e) {
log.error("JWT Verification Failed: " + e.getMessage());
return null;
} catch (Exception e) {
log.error("An unexpected error occurred: " + e.getMessage());
return null;
}
} else {
// 클라이언트 측 타임아웃 처리
log.error("Authorization header is not found");
return null;
}
}
return message;
}
}
토큰이 오지 않을 경우 명시적으로 인터셉터에서 메세지를 보내기는 어렵기때문에 null을 반환한다. null을 반환한 경우, 실제로는 메세지를 보내지 않으며 클라이언트 측은 일정 시간 내에 메세지가 안 올 경우 에러 처리를 수행할 수 있다.
클라이언트 타임아웃 코드
var socket = new SockJS('/ws-endpoint');
var stompClient = Stomp.over(socket);
var connectionTimeout = setTimeout(() => {
// 10초 후 연결이 성공하지 않으면 이 함수가 실행된다.
alert('Connection timeout. Please check your connection or try again.');
// 연결 시도를 중단하려면 socket.close()를 호출할 수 있다.
socket.close();
}, 10000); // 10초 타임아웃
stompClient.connect({}, frame => {
// 연결 성공 시 타임아웃 타이머를 취소한다.
clearTimeout(connectionTimeout);
console.log('Connected: ' + frame);
// 연결 성공 로직
}, error => {
// 연결 실패 처리
console.log('Connection error: ' + error);
});
3. 웹소켓 설정에 추가
configureClientInboundChannel 메서드를 오버라이드하여 인터셉터를 추가한다.
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final JwtChannelInterceptor jwtChannelInterceptor;
// 메시지 브로커 설정
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); // 메시지 브로커가 /topic으로 시작하는 메시지를 클라이언트로 브로드캐스팅 (1:N 통신)
config.setApplicationDestinationPrefixes("/app"); // 핸들러 메소드가 /app으로 시작하는 메시지를 처리
}
// 웹소켓 연결을 위한 endpoint 설정
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*"); // 모든 도메인에서 접근 허용
// .withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(jwtChannelInterceptor);
}
}
APIC으로 테스트할 수 있다.
WithSockJS를 주석처리한 이유는 SockJS 라이브러리를 사용할 경우 정상적으로 웹소켓 연결이 수행되지 않았기 때문이다.
'문제&해결' 카테고리의 다른 글
도커 컨테이너의 ip 주소는 왜 127.0.0.1(localhost)가 아닐까? (0) | 2024.03.09 |
---|---|
로컬, docker 컨테이너 포트 충돌 : 포트 관리 중요성 (mysql) (0) | 2024.03.09 |
도커 volume 설정 + 환경 변수를 사용하여 H2 DB Url을 동적으로 설정하기 (로컬 / Docker 환경) (0) | 2024.01.30 |
[스프링 시큐리티] Spring Security의 Filter에서 의존성 전달받기 (빈 주입) (0) | 2024.01.24 |
yml 파일이 깃허브에 올라갔을때 커밋에서 삭제하는 방법 (0) | 2023.09.29 |