-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Feature/openapi rate limit function #5267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
1b3e122
8b9d09b
1bae71f
a4c84f8
826ea74
9e2c4e8
ace5076
a622f56
4939d70
a9da81d
22ebb4f
edb9d8b
6011aeb
8463974
0fe67f5
5390f62
9126bb3
ddfd8b3
46df20c
e07de19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,11 +16,15 @@ | |
| */ | ||
| package com.ctrip.framework.apollo.openapi.filter; | ||
|
|
||
| import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; | ||
| import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; | ||
| import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; | ||
|
|
||
| import com.google.common.cache.Cache; | ||
| import com.google.common.cache.CacheBuilder; | ||
| import com.google.common.util.concurrent.RateLimiter; | ||
| import java.io.IOException; | ||
|
|
||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.TimeUnit; | ||
| import javax.servlet.Filter; | ||
| import javax.servlet.FilterChain; | ||
| import javax.servlet.FilterConfig; | ||
|
|
@@ -29,15 +33,30 @@ | |
| import javax.servlet.ServletResponse; | ||
| import javax.servlet.http.HttpServletRequest; | ||
| import javax.servlet.http.HttpServletResponse; | ||
| import org.apache.commons.lang3.tuple.ImmutablePair; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.http.HttpHeaders; | ||
|
|
||
| /** | ||
| * @author Jason Song([email protected]) | ||
| */ | ||
| public class ConsumerAuthenticationFilter implements Filter { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(ConsumerAuthenticationFilter.class); | ||
|
|
||
| private final ConsumerAuthUtil consumerAuthUtil; | ||
| private final ConsumerAuditUtil consumerAuditUtil; | ||
|
|
||
| private static final int WARMUP_MILLIS = 1000; // ms | ||
| private static final int RATE_LIMITER_CACHE_MAX_SIZE = 20000; | ||
|
|
||
| private static final int TOO_MANY_REQUESTS = 429; | ||
|
|
||
| private static final Cache<String, ImmutablePair<Long, RateLimiter>> LIMITER = CacheBuilder.newBuilder() | ||
youngzil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .expireAfterAccess(1, TimeUnit.HOURS) | ||
| .maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build(); | ||
|
|
||
| public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) { | ||
| this.consumerAuthUtil = consumerAuthUtil; | ||
| this.consumerAuditUtil = consumerAuditUtil; | ||
|
|
@@ -55,14 +74,30 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain | |
| HttpServletResponse response = (HttpServletResponse) resp; | ||
|
|
||
| String token = request.getHeader(HttpHeaders.AUTHORIZATION); | ||
| ConsumerToken consumerToken = consumerAuthUtil.getConsumerToken(token); | ||
|
|
||
| Long consumerId = consumerAuthUtil.getConsumerId(token); | ||
|
|
||
| if (consumerId == null) { | ||
| if (null == consumerToken) { | ||
| response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); | ||
| return; | ||
| } | ||
|
|
||
| Integer rateLimit = consumerToken.getRateLimit(); | ||
| if (null != rateLimit && rateLimit > 0) { | ||
| try { | ||
| ImmutablePair<Long, RateLimiter> rateLimiterPair = getOrCreateRateLimiterPair(consumerToken.getToken(), rateLimit); | ||
| long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS; | ||
nobodyiam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) { | ||
| response.sendError(TOO_MANY_REQUESTS, "Too Many Requests, the flow is limited"); | ||
| return; | ||
| } | ||
|
Comment on lines
+91
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential race condition in warmup check The warmup check compares two timestamps without synchronization, which could lead to inconsistent behavior in high-concurrency scenarios. Consider using atomic operations or moving the warmup logic into the RateLimiter itself. - long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS;
- if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) {
+ RateLimiter limiter = rateLimiterPair.getRight();
+ if (!limiter.tryAcquire()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Too many call requests, the flow is limited");
return;
}
|
||
| } catch (Exception e) { | ||
| logger.error("ConsumerAuthenticationFilter ratelimit error", e); | ||
| response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Rate limiting failed"); | ||
| return; | ||
| } | ||
| } | ||
youngzil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| long consumerId = consumerToken.getConsumerId(); | ||
| consumerAuthUtil.storeConsumerId(request, consumerId); | ||
| consumerAuditUtil.audit(request, consumerId); | ||
|
|
||
|
|
@@ -73,4 +108,14 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain | |
| public void destroy() { | ||
| //nothing | ||
| } | ||
|
|
||
| private ImmutablePair<Long, RateLimiter> getOrCreateRateLimiterPair(String key, Integer limitCount) { | ||
| try { | ||
| return LIMITER.get(key, () -> | ||
| ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount))); | ||
| } catch (ExecutionException e) { | ||
| throw new RuntimeException("Failed to create rate limiter", e); | ||
| } | ||
| } | ||
youngzil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,10 +120,10 @@ public Consumer createConsumer(Consumer consumer) { | |
| return consumerRepository.save(consumer); | ||
| } | ||
|
|
||
| public ConsumerToken generateAndSaveConsumerToken(Consumer consumer, Date expires) { | ||
| public ConsumerToken generateAndSaveConsumerToken(Consumer consumer, Integer rateLimit, Date expires) { | ||
| Preconditions.checkArgument(consumer != null, "Consumer can not be null"); | ||
|
|
||
| ConsumerToken consumerToken = generateConsumerToken(consumer, expires); | ||
| ConsumerToken consumerToken = generateConsumerToken(consumer, rateLimit, expires); | ||
|
Comment on lines
+125
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add validation for The method accepts Add parameter validation: public ConsumerToken generateAndSaveConsumerToken(Consumer consumer, Integer rateLimit, Date expires) {
Preconditions.checkArgument(consumer != null, "Consumer can not be null");
+ Preconditions.checkArgument(expires != null, "Expires date cannot be null");
+ Preconditions.checkArgument(expires.after(new Date()), "Expires date must be in the future");
ConsumerToken consumerToken = generateConsumerToken(consumer, rateLimit, expires);
|
||
| consumerToken.setId(0); | ||
|
|
||
| return consumerTokenRepository.save(consumerToken); | ||
|
|
@@ -138,12 +138,15 @@ public ConsumerToken getConsumerTokenByAppId(String appId) { | |
| return consumerTokenRepository.findByConsumerId(consumer.getId()); | ||
| } | ||
|
|
||
| public Long getConsumerIdByToken(String token) { | ||
| public ConsumerToken getConsumerTokenByToken(String token) { | ||
| if (Strings.isNullOrEmpty(token)) { | ||
| return null; | ||
| } | ||
| ConsumerToken consumerToken = consumerTokenRepository.findTopByTokenAndExpiresAfter(token, | ||
| new Date()); | ||
| return consumerTokenRepository.findTopByTokenAndExpiresAfter(token, new Date()); | ||
| } | ||
|
|
||
| public Long getConsumerIdByToken(String token) { | ||
| ConsumerToken consumerToken = getConsumerTokenByToken(token); | ||
| return consumerToken == null ? null : consumerToken.getConsumerId(); | ||
| } | ||
|
|
||
|
|
@@ -195,7 +198,8 @@ public List<ConsumerRole> assignNamespaceRoleToConsumer(String token, String app | |
| private ConsumerInfo convert( | ||
| Consumer consumer, | ||
| String token, | ||
| boolean allowCreateApplication | ||
| boolean allowCreateApplication, | ||
| Integer rateLimit | ||
| ) { | ||
| ConsumerInfo consumerInfo = new ConsumerInfo(); | ||
| consumerInfo.setConsumerId(consumer.getId()); | ||
|
|
@@ -205,6 +209,7 @@ private ConsumerInfo convert( | |
| consumerInfo.setOwnerEmail(consumer.getOwnerEmail()); | ||
| consumerInfo.setOrgId(consumer.getOrgId()); | ||
| consumerInfo.setOrgName(consumer.getOrgName()); | ||
| consumerInfo.setRateLimit(rateLimit); | ||
|
|
||
| consumerInfo.setToken(token); | ||
| consumerInfo.setAllowCreateApplication(allowCreateApplication); | ||
|
|
@@ -220,13 +225,17 @@ public ConsumerInfo getConsumerInfoByAppId(String appId) { | |
| if (consumer == null) { | ||
| return null; | ||
| } | ||
| return convert(consumer, consumerToken.getToken(), isAllowCreateApplication(consumer.getId())); | ||
| return convert(consumer, consumerToken.getToken(), isAllowCreateApplication(consumer.getId()), getRateLimit(consumer.getId())); | ||
| } | ||
|
|
||
| private boolean isAllowCreateApplication(Long consumerId) { | ||
| return isAllowCreateApplication(Collections.singletonList(consumerId)).get(0); | ||
| } | ||
|
|
||
| private Integer getRateLimit(Long consumerId) { | ||
| return getRateLimit(Collections.singletonList(consumerId)).get(0); | ||
| } | ||
|
|
||
| private List<Boolean> isAllowCreateApplication(List<Long> consumerIdList) { | ||
| Role createAppRole = getCreateAppRole(); | ||
| if (createAppRole == null) { | ||
|
|
@@ -249,6 +258,16 @@ private List<Boolean> isAllowCreateApplication(List<Long> consumerIdList) { | |
| return list; | ||
| } | ||
|
|
||
| private List<Integer> getRateLimit(List<Long> consumerIds) { | ||
| List<Integer> list = new ArrayList<>(consumerIds.size()); | ||
| List<ConsumerToken> consumerTokens = consumerTokenRepository.findByConsumerIdIn(consumerIds); | ||
| for (ConsumerToken consumerToken : consumerTokens) { | ||
| Integer rateLimit = consumerToken != null ? consumerToken.getRateLimit() : 0; | ||
| list.add(rateLimit); | ||
| } | ||
| return list; | ||
| } | ||
youngzil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private Role getCreateAppRole() { | ||
| return rolePermissionService.findRoleByRoleName(CREATE_APPLICATION_ROLE_NAME); | ||
| } | ||
|
|
@@ -311,17 +330,21 @@ public void createConsumerAudits(Iterable<ConsumerAudit> consumerAudits) { | |
| @Transactional | ||
| public ConsumerToken createConsumerToken(ConsumerToken entity) { | ||
| entity.setId(0); //for protection | ||
|
|
||
| return consumerTokenRepository.save(entity); | ||
| } | ||
|
|
||
| private ConsumerToken generateConsumerToken(Consumer consumer, Date expires) { | ||
| private ConsumerToken generateConsumerToken(Consumer consumer, Integer rateLimit, Date expires) { | ||
| long consumerId = consumer.getId(); | ||
| String createdBy = userInfoHolder.getUser().getUserId(); | ||
| Date createdTime = new Date(); | ||
|
|
||
| if (rateLimit == null || rateLimit < 0) { | ||
| rateLimit = 0; | ||
| } | ||
|
|
||
| ConsumerToken consumerToken = new ConsumerToken(); | ||
| consumerToken.setConsumerId(consumerId); | ||
| consumerToken.setRateLimit(rateLimit); | ||
youngzil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| consumerToken.setExpires(expires); | ||
| consumerToken.setDataChangeCreatedBy(createdBy); | ||
| consumerToken.setDataChangeCreatedTime(createdTime); | ||
|
|
@@ -350,7 +373,7 @@ String generateToken(String consumerAppId, Date generationTime, String consumerT | |
| (generationTime), consumerTokenSalt), Charsets.UTF_8).toString(); | ||
| } | ||
|
|
||
| ConsumerRole createConsumerRole(Long consumerId, Long roleId, String operator) { | ||
| ConsumerRole createConsumerRole(Long consumerId, Long roleId, String operator) { | ||
| ConsumerRole consumerRole = new ConsumerRole(); | ||
|
|
||
| consumerRole.setConsumerId(consumerId); | ||
|
|
@@ -389,7 +412,7 @@ private Set<String> findAppIdsByRoleIds(List<Long> roleIds) { | |
| return appIds; | ||
| } | ||
|
|
||
| List<Consumer> findAllConsumer(Pageable page){ | ||
| List<Consumer> findAllConsumer(Pageable page) { | ||
| return this.consumerRepository.findAll(page).getContent(); | ||
| } | ||
|
|
||
|
|
@@ -398,14 +421,15 @@ public List<ConsumerInfo> findConsumerInfoList(Pageable page) { | |
| List<Long> consumerIdList = consumerList.stream() | ||
| .map(Consumer::getId).collect(Collectors.toList()); | ||
| List<Boolean> allowCreateApplicationList = isAllowCreateApplication(consumerIdList); | ||
| List<Integer> rateLimitList = getRateLimit(consumerIdList); | ||
|
|
||
| List<ConsumerInfo> consumerInfoList = new ArrayList<>(consumerList.size()); | ||
|
|
||
| for (int i = 0; i < consumerList.size(); i++) { | ||
| Consumer consumer = consumerList.get(i); | ||
| // without token | ||
| ConsumerInfo consumerInfo = convert( | ||
| consumer, null, allowCreateApplicationList.get(i) | ||
| consumer, null, allowCreateApplicationList.get(i), rateLimitList.get(i) | ||
| ); | ||
| consumerInfoList.add(consumerInfo); | ||
| } | ||
|
|
@@ -414,7 +438,7 @@ public List<ConsumerInfo> findConsumerInfoList(Pageable page) { | |
| } | ||
|
|
||
| @Transactional | ||
| public void deleteConsumer(String appId){ | ||
| public void deleteConsumer(String appId) { | ||
| Consumer consumer = consumerRepository.findByAppId(appId); | ||
| if (consumer == null) { | ||
| throw new BadRequestException("ConsumerApp not exist"); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.