
HTTP Only 쿠키는 웹 브라우저와 서버 간의 HTTP(S) 통신에서만 접근 가능한 특수한 쿠키 유형입니다.
이는 Set-Cookie HTTP 응답 헤더에 HttpOnly 플래그를 설정함으로써 구현되며, 브라우저의 쿠키 저장소에서 특별한 보호를 받습니다.
| 특성 | 일반 쿠키 | HTTP Only 쿠키 |
|---|---|---|
| JavaScript 접근 | 완전한 읽기/쓰기 가능 | 접근 완전 차단 |
| 보안 수준 | 기본 | 향상됨 |
| XSS 취약성 | 높음 | 매우 낮음 |
| 구현 복잡도 | 낮음 | 중간 |
| 디버깅 용이성 | 높음 | 제한적 |
| 용도 | 클라이언트 상태 관리 | 보안 중심 데이터 저장 |
| 수명 주기 관리 | 클라이언트/서버 모두 가능 | 서버에서만 가능 |
@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;
}
}
@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"));
}
}
@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"));
}
}
@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;
}
HTTP Only 쿠키는 XSS 공격으로부터 중요한 보호를 제공하지만, 완벽한 보안을 위해서는 추가적인 조치가 필요합니다
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;
@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);
}
}
@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);
}
}
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"); // 높은 보안이 필요한 경우
}
}
@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());
}
}
}
@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);
}
}
@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();
}
}
@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);
}
}
HTTP Only 쿠키는 단독으로 완벽한 보안을 제공하지 않으며, 다음과 같은 다층적 보안 전략의 일부로 구현되어야 합니다.
@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);
}
}
@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());
}
}
@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");
}
}
}
@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;
}
}
}
@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();
}
}
@Configuration
public class MicroFrontendSecurityConfig {
@Bean
public SecurityPolicyProvider securityPolicyProvider() {
return new SecurityPolicyProvider.Builder()
.withSharedAuthenticationState(true)
.withIsolatedSessionStorage(true)
.withCookieSynchronization(
CookieSyncStrategy.DOMAIN_BASED)
.build();
}
}
@Configuration
public class AdaptiveSecurityConfig {
@Bean
public SecurityPolicyAdapter securityPolicyAdapter() {
return new SecurityPolicyAdapter.Builder()
.withThreatIntelligence(threatIntelligenceService)
.withMachineLearning(mlModelService)
.withRealTimeUpdates(true)
.withAutomaticPolicyAdjustment(true)
.build();
}
}
@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();
}
}
이러한 포괄적인 접근 방식을 통해 HTTP Only 쿠키는 현대 웹 애플리케이션의 보안을 강화하는 핵심 요소로 자리매김할 수 있습니다.
특히 마이크로서비스 아키텍처와 클라우드 네이티브 환경에서는 더욱 중요한 역할을 하며, 지속적인 보안 개선과 모니터링을 통해 효과적인 보안 체계를 구축할 수 있습니다.