Skip to content

Commit

Permalink
interceptor 기반 요청 제한 annotation 기반으로 변경 #17
Browse files Browse the repository at this point in the history
  • Loading branch information
InJun2 committed Jul 2, 2024
1 parent ec545e4 commit 00888c8
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 36 deletions.
7 changes: 0 additions & 7 deletions src/main/java/com/urlshortener/config/web/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
package com.urlshortener.config.web;

import com.urlshortener.ratelimit.interceptor.ClientIdInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final ClientIdInterceptor clientIdInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(clientIdInterceptor).addPathPatterns("/api/v1/short");
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/urlshortener/ratelimit/annotation/RateLimit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.urlshortener.ratelimit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int value();
int durationMinutes();
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,74 @@
package com.urlshortener.ratelimit.interceptor;
package com.urlshortener.ratelimit.aspect;

import com.urlshortener.error.dto.ErrorMessage;
import com.urlshortener.error.exception.url.RateLimitExceededException;
import com.urlshortener.ratelimit.annotation.RateLimit;
import com.urlshortener.ratelimit.service.RateLimitService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.urlshortener.error.dto.ErrorMessage;
import com.urlshortener.error.exception.url.RateLimitExceededException;
import com.urlshortener.ratelimit.service.RateLimitService;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Arrays;
import java.util.UUID;

@Slf4j
@RequiredArgsConstructor
@Aspect
@Component
public class ClientIdInterceptor implements HandlerInterceptor {
@RequiredArgsConstructor
public class RateLimitAspect {
private final RateLimitService rateLimitService;
private final HttpServletRequest request;
private final HttpServletResponse response;

@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, Object handler) {
var clientId = getClientIdFromCookie(request, response);
/**
* UUID 사용한 요청 횟수 제한
*
* @param rateLimit -> 일정 시간 동안 (rateLimit.value()) 특정 횟수까지 요청 제한 (rateLimit.durationMinutes())
* @throws RateLimitExceededException 일정 시간 요청 횟수 초과시 예외 발생
*/
@Before("@annotation(rateLimit)")
public void checkRateLimit(RateLimit rateLimit) {
String clientId = getClientIdFromCookie(request, response);

if (!rateLimitService.tryConsume(clientId)) {
if (!rateLimitService.tryConsume(clientId, rateLimit)) {
throw new RateLimitExceededException(ErrorMessage.RATE_LIMIT_EXCEEDED);
}

return true;
}

/**
* 쿠키 조회 및 쿠키가 없을 시 생성
*
* @param request 쿠키 조회, response 쿠키 등록
* @return clientCookie UUID
*/
private String getClientIdFromCookie(HttpServletRequest request, HttpServletResponse response) {
if (request.getCookies() == null) {
return createClientIdFromCookie(response);
}

return Arrays.stream(request.getCookies())
.filter(cookie -> "client_id".equals(cookie.getName()))
.filter(cookie -> "client-id".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElseGet(() -> createClientIdFromCookie(response));
}

/**
* 쿠키 생성 및 쿠키 저장
*
* @param response 쿠키 등록
* @return clientCookie UUID
*/
private String createClientIdFromCookie(HttpServletResponse response) {
String clientId = UUID.randomUUID().toString();
Cookie cookie = new Cookie("client_id", clientId);
Cookie cookie = new Cookie("client-id", clientId);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 183);
response.addCookie(cookie);

return clientId;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.urlshortener.ratelimit.service;

import com.urlshortener.ratelimit.annotation.RateLimit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
Expand All @@ -12,17 +12,23 @@
@Service
@RequiredArgsConstructor
public class RateLimitService {
private final RedisTemplate<String, String> redisTemplate;
private final StringRedisTemplate redisTemplate;

public boolean tryConsume(String clientId) {
String key = "rate_limit:" + clientId;
ValueOperations<String, String> ops = redisTemplate.opsForValue();
var currentCount = ops.increment(key, 1);
/**
* Client UUID 조회를 통한 요청 횟수 유효성 검사
*
* @param clientId Client UUID -> 일정 기간 내의 요청 횟수를 가져오기 위한 UUID key
* @return boolean -> 요청 횟수 성공 여부
*/
public boolean tryConsume(String clientId, RateLimit rateLimit) {
var key = "rate-limit:" + clientId;
var currentCount = redisTemplate.opsForValue().increment(key, 1);

if (currentCount == 1) {
redisTemplate.expire(key, Duration.ofMinutes(2));
var durationMinutes = rateLimit.durationMinutes();
redisTemplate.expire(key, Duration.ofMinutes(durationMinutes));
}

return currentCount <= 10;
return currentCount <= rateLimit.value();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.urlshortener.shortener.comtroller.rest;

import com.urlshortener.common.response.ResponseDto;
import com.urlshortener.ratelimit.annotation.RateLimit;
import com.urlshortener.shortener.dto.request.OriginUrlRequest;
import com.urlshortener.shortener.dto.request.ShortCodeRequest;
import com.urlshortener.shortener.service.ShortenerService;
Expand All @@ -24,6 +25,7 @@ public class ShortenerRestController {
* @param originUrlRequest
* @return shortUrl 를 반환합니다.
*/
@RateLimit(value = 10, durationMinutes = 2)
@PostMapping("/api/v1/short")
public ResponseEntity<?> createShortUrl(
@RequestBody OriginUrlRequest originUrlRequest
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/url-shortener-security

0 comments on commit 00888c8

Please sign in to comment.