Skip to content

Commit

Permalink
Api-v0.2.1
Browse files Browse the repository at this point in the history
Api-v0.2.1
  • Loading branch information
ImNM authored Feb 25, 2023
2 parents 3d49eb7 + 0283221 commit 0fa1eee
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import band.gosrock.api.auth.service.RegisterUseCase;
import band.gosrock.api.auth.service.WithDrawUseCase;
import band.gosrock.api.auth.service.helper.CookieGenerateHelper;
import band.gosrock.api.config.rateLimit.UserRateLimiter;
import band.gosrock.common.annotation.ApiErrorCodeExample;
import band.gosrock.common.annotation.DevelopOnlyApi;
import band.gosrock.infrastructure.outer.api.oauth.exception.KakaoKauthErrorCode;
Expand Down Expand Up @@ -53,6 +54,8 @@ public class AuthController {

private final CookieGenerateHelper cookieGenerateHelper;

private final UserRateLimiter rateLimiter;

@Operation(summary = "kakao oauth 링크발급 (백엔드용 )", description = "kakao 링크를 받아볼수 있습니다.")
@Tag(name = "1-2. [카카오]")
@GetMapping("/oauth/kakao/link/test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@Configuration
@RequiredArgsConstructor
@Profile({"prod", "staging"})
@Profile({"prod", "staging", "dev"})
public class ServletFilterConfig implements WebMvcConfigurer {

private final HttpContentCacheFilter httpContentCacheFilter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package band.gosrock.api.config.rateLimit;


import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import java.time.Duration;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class IPRateLimiter {
// autowiring dependencies
private final ProxyManager<String> buckets;

@Value("${throttle.overdraft}")
private long overdraft;

@Value("${throttle.greedyRefill}")
private long greedyRefill;

public Bucket resolveBucket(String key) {
Supplier<BucketConfiguration> configSupplier = getConfigSupplierForUser();
return buckets.builder().build(key, configSupplier);
}

private Supplier<BucketConfiguration> getConfigSupplierForUser() {
Refill refill = Refill.greedy(greedyRefill, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(overdraft, refill);
return () -> (BucketConfiguration.builder().addLimit(limit).build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package band.gosrock.api.config.rateLimit;


import band.gosrock.api.config.security.SecurityUtils;
import band.gosrock.api.slack.sender.SlackThrottleErrorSender;
import band.gosrock.common.dto.ErrorResponse;
import band.gosrock.common.exception.GlobalErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.bucket4j.Bucket;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;

@Component
@RequiredArgsConstructor
@Slf4j
public class ThrottlingInterceptor implements HandlerInterceptor {

private final UserRateLimiter userRateLimiter;
private final IPRateLimiter ipRateLimiter;
private final ObjectMapper objectMapper;

private final SlackThrottleErrorSender slackThrottleErrorSender;

@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
Long userId = SecurityUtils.getCurrentUserId();
Bucket bucket;
if (userId == 0L) {
// 익명 유저 ip 기반처리
String remoteAddr = request.getRemoteAddr();
bucket = ipRateLimiter.resolveBucket(remoteAddr);
} else {
// 비 익명 유저 유저 아이디 기반 처리
bucket = userRateLimiter.resolveBucket(userId.toString());
}

long availableTokens = bucket.getAvailableTokens();
log.info(userId + " : " + availableTokens);

if (bucket.tryConsume(1)) {
return true;
}

// 슬랙 알림 메시지 발송.
// limit is exceeded
ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
slackThrottleErrorSender.execute(cachingRequest, userId);
responseTooManyRequestError(request, response);

return false;
}

private void responseTooManyRequestError(
HttpServletRequest request, HttpServletResponse response) throws IOException {
ErrorResponse errorResponse =
new ErrorResponse(
GlobalErrorCode.TOO_MANY_REQUEST.getErrorReason(),
request.getRequestURL().toString());
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(errorResponse.getStatus());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package band.gosrock.api.config.rateLimit;


import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Component
public class ThrottlingWebConfigure implements WebMvcConfigurer {
private final ThrottlingInterceptor throttlingInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(throttlingInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package band.gosrock.api.config.rateLimit;


import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import java.time.Duration;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class UserRateLimiter {
// autowiring dependencies
private final ProxyManager<String> buckets;

@Value("${throttle.overdraft}")
private long overdraft;

@Value("${throttle.greedyRefill}")
private long greedyRefill;

public Bucket resolveBucket(String key) {
Supplier<BucketConfiguration> configSupplier = getConfigSupplierForUser();
return buckets.builder().build(key, configSupplier);
}

private Supplier<BucketConfiguration> getConfigSupplierForUser() {
Refill refill = Refill.greedy(greedyRefill, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(overdraft, refill);
return () -> (BucketConfiguration.builder().addLimit(limit).build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package band.gosrock.api.slack.sender;

import static com.slack.api.model.block.Blocks.divider;
import static com.slack.api.model.block.Blocks.section;
import static com.slack.api.model.block.composition.BlockCompositions.plainText;

import band.gosrock.infrastructure.config.slack.SlackErrorNotificationProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.slack.api.model.block.Blocks;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.model.block.composition.MarkdownTextObject;
import com.slack.api.model.block.composition.TextObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;

@Component
@RequiredArgsConstructor
@Slf4j
public class SlackThrottleErrorSender {
private final ObjectMapper objectMapper;

private final SlackErrorNotificationProvider slackProvider;

public void execute(ContentCachingRequestWrapper cachingRequest, Long userId)
throws IOException {
final String url = cachingRequest.getRequestURL().toString();
final String method = cachingRequest.getMethod();
final String body =
objectMapper.readTree(cachingRequest.getContentAsByteArray()).toString();
final String errorUserIP = cachingRequest.getRemoteAddr();

List<LayoutBlock> layoutBlocks = new ArrayList<>();
layoutBlocks.add(
Blocks.header(
headerBlockBuilder ->
headerBlockBuilder.text(plainText("Rate Limit Error"))));
layoutBlocks.add(divider());

MarkdownTextObject errorUserIdMarkdown =
MarkdownTextObject.builder().text("* User Id :*\n" + userId).build();
MarkdownTextObject errorUserIpMarkdown =
MarkdownTextObject.builder().text("* User IP :*\n" + errorUserIP).build();
layoutBlocks.add(
section(
section ->
section.fields(List.of(errorUserIdMarkdown, errorUserIpMarkdown))));

MarkdownTextObject methodMarkdown =
MarkdownTextObject.builder()
.text("* Request Addr :*\n" + method + " : " + url)
.build();
MarkdownTextObject bodyMarkdown =
MarkdownTextObject.builder().text("* Request Body :*\n" + body).build();
List<TextObject> fields = List.of(methodMarkdown, bodyMarkdown);
layoutBlocks.add(section(section -> section.fields(fields)));

layoutBlocks.add(divider());

slackProvider.sendNotification(layoutBlocks);
}
}
4 changes: 4 additions & 0 deletions DuDoong-Api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ server:
swagger:
user: ${SWAGGER_USER:user}
password: ${SWAGGER_PASSWORD:password}

throttle:
overdraft: ${RATE_LIMIT_OVERDRAFT:60}
greedyRefill: ${RATE_LIMIT_REFILL:60}
---
spring:
config:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public enum GlobalErrorCode implements BaseErrorCode {
TOSS_PAYMENTS_UNHANDLED(INTERNAL_SERVER, "PAYMENTS_INTERNAL_SERVER", "관리자에게 연락부탁드려요."),
BAD_LOCK_IDENTIFIER(500, "AOP_500_1", "락의 키값이 잘못 세팅 되었습니다"),
BAD_FILE_EXTENSION(BAD_REQUEST, "FILE_400_1", "파일 확장자가 잘못 되었습니다."),
TOSS_PAYMENTS_ENUM_NOT_MATCH(INTERNAL_SERVER, "INFRA_500_1", "토스페이먼츠 이넘값 관련 매칭 안된 문제입니다.");
TOSS_PAYMENTS_ENUM_NOT_MATCH(INTERNAL_SERVER, "INFRA_500_1", "토스페이먼츠 이넘값 관련 매칭 안된 문제입니다."),
TOO_MANY_REQUEST(429, "GLOBAL_429_1", "과도한 요청을 보내셨습니다. 잠시 기다려 주세요.");
private Integer status;
private String code;
private String reason;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package band.gosrock.common.exception;

public class TooManyRequestException extends DuDoongCodeException {

public static final DuDoongCodeException EXCEPTION = new TooManyRequestException();

private TooManyRequestException() {
super(GlobalErrorCode.TOO_MANY_REQUEST);
}
}
2 changes: 2 additions & 0 deletions DuDoong-Infrastructure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dependencies {
api('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')
api 'com.amazonaws:aws-java-sdk-s3control:1.12.372'
api 'io.github.openfeign:feign-jackson:12.1'
api 'com.bucket4j:bucket4j-core:8.1.1'
api 'com.bucket4j:bucket4j-jcache:8.1.1'

//for email
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package band.gosrock.infrastructure.config.redis;


import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.grid.jcache.JCacheProxyManager;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.jcache.configuration.RedissonConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -24,4 +30,21 @@ public RedissonClient redissonClient() {
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
return Redisson.create(config);
}

/** for bucket4j */
@Bean
public CacheManager cacheManager(RedissonClient redissonClient) {
CacheManager manager = Caching.getCachingProvider().getCacheManager();
Cache<Object, Object> bucket4j = manager.getCache("bucket4j");
if (bucket4j == null) {
manager.createCache("bucket4j", RedissonConfiguration.fromInstance(redissonClient));
}
return manager;
}

/** for bucket4j */
@Bean
ProxyManager<String> proxyManager(CacheManager cacheManager) {
return new JCacheProxyManager<>(cacheManager.getCache("bucket4j"));
}
}

0 comments on commit 0fa1eee

Please sign in to comment.