diff --git a/src/main/java/kr/co/knuserver/global/config/FilterConfig.java b/src/main/java/kr/co/knuserver/global/config/FilterConfig.java index 8d06b9a..e4d7fad 100644 --- a/src/main/java/kr/co/knuserver/global/config/FilterConfig.java +++ b/src/main/java/kr/co/knuserver/global/config/FilterConfig.java @@ -19,6 +19,12 @@ public class FilterConfig { @Value("${device-id.cookie.max-age}") private int cookieMaxAge; + @Value("${device-id.cookie.same-site}") + private String cookieSameSite; + + @Value("${device-id.cookie.secure}") + private boolean cookieSecure; + private final DeviceIdGenerator deviceIdGenerator; private final LikeRateLimiter likeRateLimiter; private final ObjectMapper objectMapper; @@ -33,7 +39,7 @@ public FilterRegistrationBean clientIpFilter() { @Bean public FilterRegistrationBean deviceIdCookieFilter() { - DeviceIdCookieFilter filter = new DeviceIdCookieFilter(cookieMaxAge, deviceIdGenerator); + DeviceIdCookieFilter filter = new DeviceIdCookieFilter(cookieMaxAge, cookieSameSite, cookieSecure, deviceIdGenerator); FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); bean.addUrlPatterns("/api/v1/booths/*"); bean.setOrder(2); diff --git a/src/main/java/kr/co/knuserver/global/filter/DeviceIdCookieFilter.java b/src/main/java/kr/co/knuserver/global/filter/DeviceIdCookieFilter.java index 81bf791..24645d1 100644 --- a/src/main/java/kr/co/knuserver/global/filter/DeviceIdCookieFilter.java +++ b/src/main/java/kr/co/knuserver/global/filter/DeviceIdCookieFilter.java @@ -20,10 +20,14 @@ public class DeviceIdCookieFilter implements Filter { private static final String COOKIE_NAME = "deviceId"; private final int cookieMaxAge; + private final String cookieSameSite; + private final boolean cookieSecure; private final DeviceIdGenerator deviceIdGenerator; - public DeviceIdCookieFilter(int cookieMaxAge, DeviceIdGenerator deviceIdGenerator) { + public DeviceIdCookieFilter(int cookieMaxAge, String cookieSameSite, boolean cookieSecure, DeviceIdGenerator deviceIdGenerator) { this.cookieMaxAge = cookieMaxAge; + this.cookieSameSite = cookieSameSite; + this.cookieSecure = cookieSecure; this.deviceIdGenerator = deviceIdGenerator; } @@ -41,11 +45,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String deviceId = resolveDeviceId(httpRequest); if (deviceId == null) { deviceId = deviceIdGenerator.generate(); - Cookie cookie = new Cookie(COOKIE_NAME, deviceId); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(cookieMaxAge); - httpResponse.addCookie(cookie); + String cookieHeader = buildCookieHeader(deviceId); + httpResponse.addHeader("Set-Cookie", cookieHeader); log.debug("[DeviceId] 신규 발급 ip={} deviceId={}", httpRequest.getAttribute(ClientIpFilter.CLIENT_IP_ATTRIBUTE), deviceId); } @@ -54,7 +55,20 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, response); } + private String buildCookieHeader(String deviceId) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%s=%s; Path=/; Max-Age=%d; HttpOnly; SameSite=%s", + COOKIE_NAME, deviceId, cookieMaxAge, cookieSameSite)); + if (cookieSecure) { + sb.append("; Secure"); + } + return sb.toString(); + } + private boolean isLikesPath(HttpServletRequest request) { + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return false; + } String uri = request.getRequestURI(); return uri.matches(".*/booths/[^/]+/likes$"); } diff --git a/src/main/java/kr/co/knuserver/global/ratelimit/LikeRateLimiter.java b/src/main/java/kr/co/knuserver/global/ratelimit/LikeRateLimiter.java index 3f0230c..708e342 100644 --- a/src/main/java/kr/co/knuserver/global/ratelimit/LikeRateLimiter.java +++ b/src/main/java/kr/co/knuserver/global/ratelimit/LikeRateLimiter.java @@ -11,7 +11,6 @@ public class LikeRateLimiter { private static final String KEY = "like:rate:%s:%s"; - private static final Duration WINDOW = Duration.ofMinutes(10); private final StringRedisTemplate redisTemplate; private final LikeProperties likeProperties; @@ -19,6 +18,7 @@ public class LikeRateLimiter { public boolean isAllowed(String clientIp, String deviceId) { String key = KEY.formatted(clientIp, deviceId); long maxLikes = likeProperties.rateLimit().maxLikes(); + Duration window = Duration.ofSeconds(likeProperties.rateLimit().ttlSeconds()); String countStr = redisTemplate.opsForValue().get(key); long currentCount = countStr == null ? 0 : Long.parseLong(countStr); @@ -29,7 +29,7 @@ public boolean isAllowed(String clientIp, String deviceId) { Long newCount = redisTemplate.opsForValue().increment(key); if (newCount == 1) { - redisTemplate.expire(key, WINDOW); + redisTemplate.expire(key, window); } return true; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ff73789..7f46434 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -71,6 +71,8 @@ spring: device-id: cookie: max-age: ${DEVICE_ID_COOKIE_MAX_AGE:31536000} # 기본값 1년 (초 단위) + same-site: ${DEVICE_ID_COOKIE_SAME_SITE:Lax} + secure: ${DEVICE_ID_COOKIE_SECURE:true} like: rate-limit: