Skip to content
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

Feature/openapi rate limit function #5267

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b3e122
feat(portal): Add current limiting function to ConsumerToken
youngzil Nov 3, 2024
8b9d09b
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 3, 2024
1bae71f
fix:add CHANGES.md and optimize some codes
youngzil Nov 3, 2024
a4c84f8
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 10, 2024
826ea74
feat(openapi): 重构 ConsumerToken 限流功能
youngzil Nov 17, 2024
9e2c4e8
refactor(Consumer): Spelling error in attribute
youngzil Nov 17, 2024
ace5076
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 20, 2024
a622f56
refactor(openapi): Refactor consumer authentication filters and relat…
youngzil Nov 21, 2024
4939d70
test(apollo-portal): Optimize the rate limiting test of ConsumerAuthe…
youngzil Nov 21, 2024
a9da81d
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 23, 2024
22ebb4f
featapi(open): Updated management page to show consumer rate limit in…
youngzil Nov 23, 2024
edb9d8b
fix(portal): Optimized the robustness of the code
youngzil Nov 23, 2024
6011aeb
fix(portal): Optimized the robustness of the code
youngzil Nov 23, 2024
8463974
fix(portal): fix unit test
youngzil Nov 23, 2024
0fe67f5
Merge branch 'refs/heads/master' into feature/openapi-rate-limit-func…
youngzil Nov 26, 2024
5390f62
feat(openapi): Added consumer rate limit query function and optimized…
youngzil Nov 26, 2024
9126bb3
fix(portal): Fix the processing logic when the consumer obtains an em…
youngzil Nov 27, 2024
ddfd8b3
refactor(ConsumerService): Optimize the implementation of getRateLimi…
youngzil Nov 27, 2024
46df20c
Update CHANGES.md
nobodyiam Nov 28, 2024
e07de19
Update CHANGES.md
nobodyiam Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ Apollo 2.4.0
* [Fix: bump xstream from 1.4.20 to 1.4.21 to fix CVE-2024-47072](https://github.com/apolloconfig/apollo/pull/5280)
* [Feature: highlight diffs for properties](https://github.com/apolloconfig/apollo/pull/5282)

* [Feature: Added current limiting function to ConsumerToken](https://github.com/apolloconfig/apollo/pull/5267)
nobodyiam marked this conversation as resolved.
Show resolved Hide resolved
------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
return new BadRequestException("orgId can not be blank");
}

public static BadRequestException rateLimitIsInvalid() {
return new BadRequestException("rate limit must be greater than 1");

Check warning on line 41 in apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java

View check run for this annotation

Codecov / codecov/patch

apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java#L41

Added line #L41 was not covered by tests
}

public static BadRequestException itemAlreadyExists(String itemKey) {
return new BadRequestException("item already exists for itemKey:%s", itemKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.ctrip.framework.apollo.common.entity.BaseEntity;

import javax.validation.constraints.PositiveOrZero;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

Expand All @@ -41,6 +42,10 @@
@Column(name = "`Token`", nullable = false)
private String token;

@PositiveOrZero
@Column(name = "`RateLimit`", nullable = false)
private Integer rateLimit;

@Column(name = "`Expires`", nullable = false)
private Date expires;

Expand All @@ -60,6 +65,14 @@
this.token = token;
}

public Integer getRateLimit() {
return rateLimit;
}

public void setRateLimit(Integer rateLimit) {
this.rateLimit = rateLimit;
}

public Date getExpires() {
return expires;
}
Expand All @@ -71,6 +84,7 @@
@Override
public String toString() {
return toStringHelper().add("consumerId", consumerId).add("token", token)
.add("rateLimit", rateLimit)

Check warning on line 87 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java#L87

Added line #L87 was not covered by tests
.add("expires", expires).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -55,14 +74,30 @@
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;
}
} catch (Exception e) {
logger.error("ConsumerAuthenticationFilter ratelimit error", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Rate limiting failed");
return;

Check warning on line 96 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java#L93-L96

Added lines #L93 - L96 were not covered by tests
}
}

long consumerId = consumerToken.getConsumerId();
consumerAuthUtil.storeConsumerId(request, consumerId);
consumerAuditUtil.audit(request, consumerId);

Expand All @@ -73,4 +108,14 @@
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);

Check warning on line 117 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java#L116-L117

Added lines #L116 - L117 were not covered by tests
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.ctrip.framework.apollo.openapi.entity.ConsumerToken;

import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.Date;
Expand All @@ -35,4 +36,7 @@ public interface ConsumerTokenRepository extends PagingAndSortingRepository<Cons
ConsumerToken findTopByTokenAndExpiresAfter(String token, Date validDate);

ConsumerToken findByConsumerId(Long consumerId);

List<ConsumerToken> findByConsumerIdIn(List<Long> consumerIds);

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.google.common.hash.Hashing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.time.FastDateFormat;
import org.springframework.data.domain.Pageable;
Expand All @@ -56,6 +57,7 @@
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.util.CollectionUtils;

/**
* @author Jason Song([email protected])
Expand Down Expand Up @@ -120,10 +122,10 @@
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);

Check warning on line 128 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L128

Added line #L128 was not covered by tests
Comment on lines +125 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for expires parameter

The method accepts expires without validation, which could lead to issues if null or past dates are provided.

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);

Committable suggestion skipped: line range outside the PR's diff.

consumerToken.setId(0);

return consumerTokenRepository.save(consumerToken);
Expand All @@ -138,12 +140,15 @@
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();
}

Expand Down Expand Up @@ -195,7 +200,8 @@
private ConsumerInfo convert(
Consumer consumer,
String token,
boolean allowCreateApplication
boolean allowCreateApplication,
Integer rateLimit
) {
ConsumerInfo consumerInfo = new ConsumerInfo();
consumerInfo.setConsumerId(consumer.getId());
Expand All @@ -205,6 +211,7 @@
consumerInfo.setOwnerEmail(consumer.getOwnerEmail());
consumerInfo.setOrgId(consumer.getOrgId());
consumerInfo.setOrgName(consumer.getOrgName());
consumerInfo.setRateLimit(rateLimit);

consumerInfo.setToken(token);
consumerInfo.setAllowCreateApplication(allowCreateApplication);
Expand All @@ -220,13 +227,21 @@
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) {
List<Integer> list = getRateLimit(Collections.singletonList(consumerId));
if (CollectionUtils.isEmpty(list)) {
return 0;

Check warning on line 240 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L240

Added line #L240 was not covered by tests
}
return list.get(0);
}

private List<Boolean> isAllowCreateApplication(List<Long> consumerIdList) {
Role createAppRole = getCreateAppRole();
if (createAppRole == null) {
Expand All @@ -249,6 +264,19 @@
return list;
}

private List<Integer> getRateLimit(List<Long> consumerIds) {
List<ConsumerToken> consumerTokens = consumerTokenRepository.findByConsumerIdIn(consumerIds);
Map<Long, Integer> consumerRateLimits = consumerTokens.stream()
.collect(Collectors.toMap(
ConsumerToken::getConsumerId,
consumerToken -> consumerToken.getRateLimit() != null ? consumerToken.getRateLimit() : 0
));

return consumerIds.stream()
.map(id -> consumerRateLimits.getOrDefault(id, 0))
.collect(Collectors.toList());
}

private Role getCreateAppRole() {
return rolePermissionService.findRoleByRoleName(CREATE_APPLICATION_ROLE_NAME);
}
Expand Down Expand Up @@ -311,17 +339,21 @@
@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;

Check warning on line 351 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L351

Added line #L351 was not covered by tests
}

ConsumerToken consumerToken = new ConsumerToken();
consumerToken.setConsumerId(consumerId);
consumerToken.setRateLimit(rateLimit);

Check warning on line 356 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L356

Added line #L356 was not covered by tests
consumerToken.setExpires(expires);
consumerToken.setDataChangeCreatedBy(createdBy);
consumerToken.setDataChangeCreatedTime(createdTime);
Expand Down Expand Up @@ -350,7 +382,7 @@
(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);
Expand Down Expand Up @@ -389,7 +421,7 @@
return appIds;
}

List<Consumer> findAllConsumer(Pageable page){
List<Consumer> findAllConsumer(Pageable page) {
return this.consumerRepository.findAll(page).getContent();
}

Expand All @@ -398,14 +430,15 @@
List<Long> consumerIdList = consumerList.stream()
.map(Consumer::getId).collect(Collectors.toList());
List<Boolean> allowCreateApplicationList = isAllowCreateApplication(consumerIdList);
List<Integer> rateLimitList = getRateLimit(consumerIdList);

Check warning on line 433 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L433

Added line #L433 was not covered by tests

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)

Check warning on line 441 in apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

View check run for this annotation

Codecov / codecov/patch

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java#L441

Added line #L441 was not covered by tests
);
consumerInfoList.add(consumerInfo);
}
Expand All @@ -414,7 +447,7 @@
}

@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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.ctrip.framework.apollo.openapi.util;

import com.ctrip.framework.apollo.openapi.entity.ConsumerToken;
import com.ctrip.framework.apollo.openapi.service.ConsumerService;
import org.springframework.stereotype.Service;

Expand All @@ -37,6 +38,10 @@ public Long getConsumerId(String token) {
return consumerService.getConsumerIdByToken(token);
}

public ConsumerToken getConsumerToken(String token) {
return consumerService.getConsumerTokenByToken(token);
}
nobodyiam marked this conversation as resolved.
Show resolved Hide resolved

public void storeConsumerId(HttpServletRequest request, Long consumerId) {
request.setAttribute(CONSUMER_ID, consumerId);
}
Expand Down
Loading
Loading