HTTP Only 쿠키 구현 정리

1. HTTP Only 쿠키의 기술적 이해

1.1 HTTP Only 쿠키의 정의와 동작 원리

HTTP Only 쿠키는 웹 브라우저와 서버 간의 HTTP(S) 통신에서만 접근 가능한 특수한 쿠키 유형입니다.

이는 Set-Cookie HTTP 응답 헤더에 HttpOnly 플래그를 설정함으로써 구현되며, 브라우저의 쿠키 저장소에서 특별한 보호를 받습니다.

주요특징

  • DOM을 통한 접근 완전 차단 (document.cookie 접근 불가)
  • XMLHttpRequest나 Fetch API를 통한 요청에는 자동 포함
  • 네이티브 JavaScript API를 통한 쿠키 조작 방지
  • 브라우저 개발자 도구에서도 수정 불가

1.2 일반 쿠키와 HTTP Only 쿠키의 상세 비교

특성일반 쿠키HTTP Only 쿠키
JavaScript 접근완전한 읽기/쓰기 가능접근 완전 차단
보안 수준기본향상됨
XSS 취약성높음매우 낮음
구현 복잡도낮음중간
디버깅 용이성높음제한적
용도클라이언트 상태 관리보안 중심 데이터 저장
수명 주기 관리클라이언트/서버 모두 가능서버에서만 가능

2. 고급 구현 전략

2.1 Spring Boot에서의 세션 기반 구현

@Configuration
public class SecurityConfig {
    
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite("Strict");
        serializer.setCookiePath("/");
        serializer.setCookieName("SESSIONID");
        serializer.setCookieMaxAge(Duration.ofHours(1).toSeconds());
        
        // 도메인 설정 (하위 도메인 포함)
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        
        return serializer;
    }
    
    @Bean
    public WebSessionManager webSessionManager(CookieSerializer cookieSerializer) {
        DefaultCookieWebSessionManager manager = new DefaultCookieWebSessionManager();
        manager.setCookieSerializer(cookieSerializer);
        return manager;
    }
}

2.2 JWT와 조합한 고급 구현

@Service
public class JwtTokenService {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        
        return Jwts.builder()
            .setClaims(claims)
            .setExpiration(new Date(System.currentTimeMillis() + 3600000))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    @PostMapping("/auth/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, 
                                 HttpServletResponse response) {
        // 인증 로직...
        
        String jwt = generateToken(userDetails);
        
        ResponseCookie jwtCookie = ResponseCookie.from("JWT", jwt)
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(Duration.ofHours(1))
            .sameSite("Strict")
            .domain("example.com")
            .build();
            
        response.addHeader(HttpHeaders.SET_COOKIE, jwtCookie.toString());
        
        return ResponseEntity.ok()
            .body(new LoginResponse("Authentication successful"));
    }
}

2.2 JWT와 조합한 고급 구현

@Service
public class JwtTokenService {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        
        return Jwts.builder()
            .setClaims(claims)
            .setExpiration(new Date(System.currentTimeMillis() + 3600000))
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    @PostMapping("/auth/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, 
                                 HttpServletResponse response) {
        // 인증 로직...
        
        String jwt = generateToken(userDetails);
        
        ResponseCookie jwtCookie = ResponseCookie.from("JWT", jwt)
            .httpOnly(true)
            .secure(true)
            .path("/")
            .maxAge(Duration.ofHours(1))
            .sameSite("Strict")
            .domain("example.com")
            .build();
            
        response.addHeader(HttpHeaders.SET_COOKIE, jwtCookie.toString());
        
        return ResponseEntity.ok()
            .body(new LoginResponse("Authentication successful"));
    }
}

2.3 보안 강화를 위한 추가 설정

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler()))
        .cors(cors -> cors
            .configurationSource(corsConfigurationSource()))
        .headers(headers -> headers
            .frameOptions().deny()
            .xssProtection().enable()
            .contentSecurityPolicy("default-src 'self'"))
        .build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://frontend.example.com"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-XSRF-TOKEN"));
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

3. 보안 위협 대응 전략

3.1 XSS 공격 방어 메커니즘

HTTP Only 쿠키는 XSS 공격으로부터 중요한 보호를 제공하지만, 완벽한 보안을 위해서는 추가적인 조치가 필요합니다

1. 콘텐츠 보안 정책(CSP) 구현

Content-Security-Policy: default-src 'self';
                        script-src 'self' 'nonce-randomValue';
                        style-src 'self' 'unsafe-inline';
                        img-src 'self' data: https:;
                        connect-src 'self' api.example.com;

2. 입력 데이터 검증 및 이스케이프

@Component
public class XssProtectionFilter implements Filter {
    
    private final HtmlPolicyBuilder policyBuilder;
    
    public XssProtectionFilter() {
        this.policyBuilder = new HtmlPolicyBuilder()
            .allowElements("b", "i", "u", "strong", "em")
            .allowUrlProtocols("https")
            .requireRelNofollowOnLinks();
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest wrappedRequest = new XssRequestWrapper(
            (HttpServletRequest) request, policyBuilder.toFactory());
        chain.doFilter(wrappedRequest, response);
    }
}

3.2 CSRF 공격 방어 전략

@Component
public class CsrfTokenManager {
    
    private static final SecureRandom secureRandom = new SecureRandom();
    
    public String generateToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }
    
    public Cookie createCsrfCookie(String token) {
        Cookie cookie = new Cookie("XSRF-TOKEN", token);
        cookie.setHttpOnly(false); // JavaScript에서 읽을 수 있어야 함
        cookie.setSecure(true);
        cookie.setPath("/");
        cookie.setMaxAge(-1); // 세션 쿠키
        return cookie;
    }
    
    @Override
    public void doFilterInternal(HttpServletRequest request,
                               HttpServletResponse response,
                               FilterChain filterChain) throws ServletException, IOException {
        if (isNonGetRequest(request)) {
            String cookieToken = extractCsrfTokenFromCookie(request);
            String headerToken = request.getHeader("X-XSRF-TOKEN");
            
            if (!isValidToken(cookieToken, headerToken)) {
                throw new CsrfException("CSRF token validation failed");
            }
        }
        
        filterChain.doFilter(request, response);
    }
}

2. SameSite 속성의 전략적 활용

public class CookieConfig {
    
    public static ResponseCookie.ResponseCookieBuilder getBaseCookieBuilder(
            String name, String value) {
        return ResponseCookie.from(name, value)
            .httpOnly(true)
            .secure(true)
            .path("/")
            .sameSite("Lax") // 기본값으로 Lax 사용
            .domain("example.com");
    }
    
    public static ResponseCookie.ResponseCookieBuilder getStrictCookieBuilder(
            String name, String value) {
        return getBaseCookieBuilder(name, value)
            .sameSite("Strict"); // 높은 보안이 필요한 경우
    }
}

4. 성능 최적화 및 모니터링

4.1 쿠키 크기 최적화

@Component
public class CookieOptimizer {
    
    private static final int MAX_COOKIE_SIZE = 4096; // 4KB
    
    public String optimizeJwtPayload(Map<String, Object> claims) {
        // 필수 클레임만 포함
        Map<String, Object> essentialClaims = new HashMap<>();
        essentialClaims.put("sub", claims.get("sub"));
        essentialClaims.put("exp", claims.get("exp"));
        
        // 역할 정보 압축
        List<String> roles = (List<String>) claims.get("roles");
        if (roles != null) {
            essentialClaims.put("roles", roles.stream()
                .map(role -> role.replace("ROLE_", ""))
                .collect(Collectors.joining(",")));
        }
        
        return new ObjectMapper().writeValueAsString(essentialClaims);
    }
    
    public void validateCookieSize(Cookie cookie) {
        if (cookie.getValue().length() > MAX_COOKIE_SIZE) {
            throw new CookieSizeExceededException(
                "Cookie size exceeds 4KB limit: " + cookie.getName());
        }
    }
}

4.2 성능 모니터링

@Aspect
@Component
public class CookiePerformanceMonitor {
    
    private final MeterRegistry meterRegistry;
    
    @Around("@annotation(org.springframework.web.bind.annotation.CookieValue)")
    public Object monitorCookieAccess(ProceedingJoinPoint joinPoint) throws Throwable {
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            return joinPoint.proceed();
        } finally {
            sample.stop(meterRegistry.timer("cookie.access.time",
                "method", joinPoint.getSignature().getName()));
        }
    }
    
    @Scheduled(fixedRate = 300000) // 5분마다 실행
    public void reportCookieMetrics() {
        int totalCookies = meterRegistry.get("cookie.count").counter().count();
        double avgAccessTime = meterRegistry.get("cookie.access.time")
            .timer().mean(TimeUnit.MILLISECONDS);
        
        log.info("Cookie Metrics - Total: {}, Avg Access Time: {}ms",
            totalCookies, avgAccessTime);
    }
}

5. 마이크로서비스 아키텍처에서의 HTTP Only 쿠키

5.1 API Gateway에서의 쿠키 관리

@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("auth_service", r -> r.path("/auth/**")
                .filters(f -> f
                    .rewritePath("/auth/(?<segment>.*)", "/${segment}")
                    .addResponseHeader("Set-Cookie",
                        "SESSION=#{cookie.SESSION}; HttpOnly; Secure; SameSite=Strict")
                    .removeRequestHeader("Cookie"))
                .uri("lb://auth-service"))
            .build();
    }
}

5.2 서비스 간 인증 전파

@Component
public class AuthenticationPropagationFilter implements WebFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                            WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        List<HttpCookie> cookies = request.getCookies()
            .get("SESSION");
        
        if (cookies != null && !cookies.isEmpty()) {
            String sessionId = cookies.get(0).getValue();
            
            return chain.filter(
                exchange.mutate()
                    .request(request.mutate()
                        .header("X-Auth-Token", sessionId)
                        .build())
                    .build());
        }
        
        return chain.filter(exchange);
    }
}

6. 결론: HTTP Only 쿠키의 전략적 활용 (계속)

6.1 보안 계층화 전략

HTTP Only 쿠키는 단독으로 완벽한 보안을 제공하지 않으며, 다음과 같은 다층적 보안 전략의 일부로 구현되어야 합니다.

1. 토큰 관리 전략

@Configuration
public class TokenSecurityConfig {
    @Bean
    public TokenStrategy tokenStrategy() {
        return new TokenStrategy.Builder()
            .withHttpOnlyCookie(true)
            .withSecureCookie(true)
            .withRotation(Duration.ofMinutes(30))
            .withJwtSigningKey(keyProvider.getSigningKey())
            .withRefreshTokenRepository(refreshTokenRepository)
            .build();
    }
    
    @Bean
    public TokenRotationService tokenRotationService(TokenStrategy tokenStrategy) {
        return new TokenRotationService(tokenStrategy, clock);
    }
}

2. 세션 관리 강화

@Configuration
public class SessionSecurityConfig {
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
    
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
    
    @Bean
    public SessionManagementConfigurer<HttpSecurity> sessionManagement() {
        return new SessionManagementConfigurer<HttpSecurity>()
            .maximumSessions(1)
            .expiredUrl("/login?expired")
            .sessionRegistry(sessionRegistry());
    }
}

6.2 운영 환경에서의 모니터링 및 대응

1. 실시간 보안 모니터링

@Service
public class SecurityMonitoringService {
    private final AlertService alertService;
    private final MetricsService metricsService;
    
    @Scheduled(fixedRate = 60000) // 1분마다 실행
    public void monitorSecurityMetrics() {
        Map<String, Long> metrics = metricsService.getSecurityMetrics();
        
        // 비정상적인 쿠키 접근 패턴 감지
        if (metrics.get("invalid_cookie_attempts") > THRESHOLD) {
            alertService.sendAlert(AlertLevel.HIGH,
                "Unusual cookie access patterns detected");
        }
        
        // 세션 하이재킹 시도 감지
        if (metrics.get("concurrent_session_attempts") > THRESHOLD) {
            alertService.sendAlert(AlertLevel.CRITICAL,
                "Potential session hijacking detected");
        }
    }
}

2. 인시던트 대응 자동화

@Component
public class SecurityIncidentHandler {
    @EventListener
    public void handleSecurityEvent(SecurityEvent event) {
        switch (event.getType()) {
            case COOKIE_TAMPERING:
                invalidateAffectedSessions(event);
                notifySecurityTeam(event);
                break;
            case CSRF_ATTACK:
                blockSuspiciousIPs(event);
                rotateSecurityTokens(event);
                break;
            case XSS_ATTEMPT:
                enhanceContentSecurityPolicy(event);
                logAttackPatterns(event);
                break;
        }
    }
}

6.3 도메인 기반 보안 정책

1. 멀티 도메인 환경에서의 쿠키 관리

@Configuration
public class MultiDomainCookieConfig {
    @Bean
    public CookiePolicyEnforcer cookiePolicyEnforcer() {
        return new CookiePolicyEnforcer.Builder()
            .withDomainMapping("api.example.com", CookiePolicy.API)
            .withDomainMapping("auth.example.com", CookiePolicy.AUTH)
            .withDomainMapping("*.example.com", CookiePolicy.GENERAL)
            .withCrossOriginStrategy(CrossOriginStrategy.STRICT)
            .build();
    }
}

2. 마이크로프론트엔드 아키텍처 지원

@Configuration
public class MicroFrontendSecurityConfig {
    @Bean
    public SecurityPolicyProvider securityPolicyProvider() {
        return new SecurityPolicyProvider.Builder()
            .withSharedAuthenticationState(true)
            .withIsolatedSessionStorage(true)
            .withCookieSynchronization(
                CookieSyncStrategy.DOMAIN_BASED)
            .build();
    }
}

6.4 미래 지향적 보안 전략

1. 새로운 웹 표준 대응

  • Privacy Sandbox 지원
  • FedCM (Federated Credential Management) 통합
  • Private State Tokens 활용

2. 보안 정책 자동화

@Configuration
public class AdaptiveSecurityConfig {
    @Bean
    public SecurityPolicyAdapter securityPolicyAdapter() {
        return new SecurityPolicyAdapter.Builder()
            .withThreatIntelligence(threatIntelligenceService)
            .withMachineLearning(mlModelService)
            .withRealTimeUpdates(true)
            .withAutomaticPolicyAdjustment(true)
            .build();
    }
}

6.5 최종 확인 사항

1. 보안 체크리스트

  • HTTP Only + Secure 플래그 필수 적용
  • SameSite 속성 적절한 구성
  • CSRF 토큰 검증 구현
  • Content Security Policy 헤더 설정
  • 정기적인 보안 감사 수행

2. 개발 프로세스 통합

@Configuration
public class SecurityDevelopmentConfig {
    @Bean
    public SecurityChecklistEnforcer securityEnforcer() {
        return new SecurityChecklistEnforcer.Builder()
            .withPreCommitHooks(true)
            .withCiCdIntegration(true)
            .withSecurityScanners(Arrays.asList(
                new XssScanner(),
                new CsrfScanner(),
                new CookieScanner()))
            .withAutomatedTests(true)
            .build();
    }
}

3. 지속적인 보안 개선

  • 정기적인 보안 평가 및 업데이트
  • 새로운 보안 위협에 대한 모니터링

이러한 포괄적인 접근 방식을 통해 HTTP Only 쿠키는 현대 웹 애플리케이션의 보안을 강화하는 핵심 요소로 자리매김할 수 있습니다.

특히 마이크로서비스 아키텍처와 클라우드 네이티브 환경에서는 더욱 중요한 역할을 하며, 지속적인 보안 개선과 모니터링을 통해 효과적인 보안 체계를 구축할 수 있습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글