Websocket 보안 방식 변경 방법 2
저번 포스팅에서 했던 웹소켓 보안 방식에 치명적인 결함을 발견하여 보안 방식을 업데이트 하기로 했다.
웹소켓 보안 방식 업데이트: 쿠키 방식 도입.
- 구현:
-
- 웹소켓 연결을 열고, 첫 메시지 헤더에 토큰을 보내 인증 수행.
- 발견된 취약점:
-
- Postman 같은 도구로 웹소켓을 임의로 연결하고
/user/queue
를 구독하면 보안이 뚫리는 치명적 결함 발견. - 누구나 연결 후 메시지를 보낼 수 있어 인증 우회 가능성 존재.
- Postman 같은 도구로 웹소켓을 임의로 연결하고
- 추가 문제:
-
- 웹소켓 통신 헤더에 토큰을 넣는 방식은 권장되지 않음 (개발자 커뮤니티 및 다수 문서 확인).
- URL에 쿼리 파라미터로 토큰을 노출시키는 방식도 고민했으나, 보안상 여전히 취약.
기존 방식 (첫 메시지 헤더 방식)의 문제점
- 1. 쿠키 생성 및 전달
-
- 유저가 처음 로그인 시, 서버에서 토큰을 생성하고 이를 쿠키에 저장.
@Component public class JwtUtil { private static final String SECRET_KEY = "syoo-secret-key-tlzmflt-zl"; //키 값 private static final long EXPIRATION_TIME = 86400000; // 24 hours public String generateToken(Integer userId) { return Jwts.builder() .setSubject(userId.toString()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Integer getUserIdFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); return Integer.parseInt(claims.getSubject()); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } }
- 이 쿠키를 클라이언트에게 전달
- 쿠키는 모든 후속 요청(HTTP 및 웹소켓)에 자동으로 포함되어 서버로 전송됨.
- 2. 웹소켓 인터셉터 부활
-
- 웹소켓 연결 시 인터셉터
HandshakeInterceptor
를 다시 활성화. - 인터셉터에서
HttpServletRequest.getCookies()
로 쿠키를 읽고, 토큰을 추출해 검증 (예:JwtUtil
사용).
@Slf4j public class JwtHandshakeInterceptor implements HandshakeInterceptor { private final JwtUtil jwtUtil; public JwtHandshakeInterceptor(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map
attributes) { if (!(request instanceof ServletServerHttpRequest servletRequest)) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } HttpServletRequest httpServletRequest = servletRequest.getServletRequest(); Cookie[] cookies = httpServletRequest.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("Authorization".equals(cookie.getName())) { String token = cookie.getValue(); if (jwtUtil.validateToken(token)) { Integer userId = jwtUtil.getUserIdFromToken(token); attributes.put("userId", userId); log.info("Websocket, Validated UserID: {}", userId); return true; } System.out.println("Token validation failed"); response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } } } response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} } - 토큰이 유효하면 웹소켓 연결 허용, 그렇지 않으면 연결 차단.
- 웹소켓 연결 시 인터셉터
- 3. 인증 흐름
-
- 클라이언트는 별도로 토큰을 관리하거나 전송할 필요 없이, 쿠키만으로 인증.
- 서버는 연결 시점에서 쿠키를 확인해 즉시 인증 여부 결정.
새로운 방식: 쿠키 기반 인증
- 보안 강화:
-
- 토큰이 URL이나 메시지 헤더에 노출되지 않음.
- Postman 같은 도구로 임의 연결해도 쿠키 없이는 인증 불가.
- 클라이언트 간소화:
-
localStorage
에 토큰 저장 불필요 → 클라이언트 코드 단순화 및 XSS 위험 감소.- Postman 같은 도구로 임의 연결해도 쿠키 없이는 인증 불가.
- 필터 간소화:
-
- HTTP 요청 필터
JwtFilter
에서도 헤더 대신 쿠키로 토큰 확인 가능 → 토큰 전송 방식 통일.
- HTTP 요청 필터
- 자동화:
-
- 쿠키는 브라우저에서 자동으로 관리되므로, 매 요청마다 토큰을 수동으로 붙일 필요 없음.
장점
-
- 변경 전: 첫 메시지 헤더 방식 → 취약점 노출, 비권장 방식.
- 변경 후: 쿠키 기반 + 인터셉터 → 보안성↑, 편리성↑, 통일성↑.
- 결과: 웹소켓과 HTTP 모두 쿠키로 인증 처리, 클라이언트 로직 단순화, 보안 결함 해결.
결론
Posted on 2025.02.21