Websocket 보안 방식 변경 방법 2


저번 포스팅에서 했던 웹소켓 보안 방식에 치명적인 결함을 발견하여 보안 방식을 업데이트 하기로 했다.

웹소켓 보안 방식 업데이트: 쿠키 방식 도입.


기존 방식 (첫 메시지 헤더 방식)의 문제점

구현:
  • 웹소켓 연결을 열고, 첫 메시지 헤더에 토큰을 보내 인증 수행.
발견된 취약점:
  • Postman 같은 도구로 웹소켓을 임의로 연결하고 /user/queue를 구독하면 보안이 뚫리는 치명적 결함 발견.
  • 누구나 연결 후 메시지를 보낼 수 있어 인증 우회 가능성 존재.
추가 문제:
  • 웹소켓 통신 헤더에 토큰을 넣는 방식은 권장되지 않음 (개발자 커뮤니티 및 다수 문서 확인).
  • 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 모두 쿠키로 인증 처리, 클라이언트 로직 단순화, 보안 결함 해결.


Posted on 2025.02.21