diff --git a/CHANGES.md b/CHANGES.md index c584794bdc7..d0296dbee35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ Apollo 2.4.0 * [Fix: ensure clusters order in envClusters open api](https://github.com/apolloconfig/apollo/pull/5277) * [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: Add rate limiting function to ConsumerToken](https://github.com/apolloconfig/apollo/pull/5267) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1) diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java index 567fd6b375c..22be8723f03 100644 --- a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java +++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java @@ -37,6 +37,10 @@ public static BadRequestException orgIdIsBlank() { return new BadRequestException("orgId can not be blank"); } + public static BadRequestException rateLimitIsInvalid() { + return new BadRequestException("rate limit must be greater than 1"); + } + public static BadRequestException itemAlreadyExists(String itemKey) { return new BadRequestException("item already exists for itemKey:%s", itemKey); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java index baa1c0dc7c7..75893cff567 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/entity/ConsumerToken.java @@ -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; @@ -41,6 +42,10 @@ public class ConsumerToken extends BaseEntity { @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; @@ -60,6 +65,14 @@ public void setToken(String token) { this.token = token; } + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(Integer rateLimit) { + this.rateLimit = rateLimit; + } + public Date getExpires() { return expires; } @@ -71,6 +84,7 @@ public void setExpires(Date expires) { @Override public String toString() { return toStringHelper().add("consumerId", consumerId).add("token", token) + .add("rateLimit", rateLimit) .add("expires", expires).toString(); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java index c08de6dd505..1aaa8f1d4ec 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilter.java @@ -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(song_s@ctrip.com) */ 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> LIMITER = CacheBuilder.newBuilder() + .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 rateLimiterPair = getOrCreateRateLimiterPair(consumerToken.getToken(), rateLimit); + long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS; + 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; + } + } + + 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 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); + } + } + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerTokenRepository.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerTokenRepository.java index 456dee7717b..8519149ad15 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerTokenRepository.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/repository/ConsumerTokenRepository.java @@ -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; @@ -35,4 +36,7 @@ public interface ConsumerTokenRepository extends PagingAndSortingRepository findByConsumerIdIn(List consumerIds); + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java index 90164feea7f..e0676a18655 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java @@ -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; @@ -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(song_s@ctrip.com) @@ -120,10 +122,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); consumerToken.setId(0); return consumerTokenRepository.save(consumerToken); @@ -138,12 +140,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 +200,8 @@ public List 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 +211,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 +227,21 @@ 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) { + List list = getRateLimit(Collections.singletonList(consumerId)); + if (CollectionUtils.isEmpty(list)) { + return 0; + } + return list.get(0); + } + private List isAllowCreateApplication(List consumerIdList) { Role createAppRole = getCreateAppRole(); if (createAppRole == null) { @@ -249,6 +264,19 @@ private List isAllowCreateApplication(List consumerIdList) { return list; } + private List getRateLimit(List consumerIds) { + List consumerTokens = consumerTokenRepository.findByConsumerIdIn(consumerIds); + Map 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); } @@ -311,17 +339,21 @@ public void createConsumerAudits(Iterable 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); consumerToken.setExpires(expires); consumerToken.setDataChangeCreatedBy(createdBy); consumerToken.setDataChangeCreatedTime(createdTime); @@ -350,7 +382,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 +421,7 @@ private Set findAppIdsByRoleIds(List roleIds) { return appIds; } - List findAllConsumer(Pageable page){ + List findAllConsumer(Pageable page) { return this.consumerRepository.findAll(page).getContent(); } @@ -398,6 +430,7 @@ public List findConsumerInfoList(Pageable page) { List consumerIdList = consumerList.stream() .map(Consumer::getId).collect(Collectors.toList()); List allowCreateApplicationList = isAllowCreateApplication(consumerIdList); + List rateLimitList = getRateLimit(consumerIdList); List consumerInfoList = new ArrayList<>(consumerList.size()); @@ -405,7 +438,7 @@ public List findConsumerInfoList(Pageable page) { 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 +447,7 @@ public List 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"); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java index 83d5e02ab4e..1eff110210d 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java @@ -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; @@ -37,6 +38,10 @@ public Long getConsumerId(String token) { return consumerService.getConsumerIdByToken(token); } + public ConsumerToken getConsumerToken(String token) { + return consumerService.getConsumerTokenByToken(token); + } + public void storeConsumerId(HttpServletRequest request, Long consumerId) { request.setAttribute(CONSUMER_ID, consumerId); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java index 0c7a5141731..0bc1d08e56e 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java @@ -81,13 +81,21 @@ public ConsumerInfo create( throw BadRequestException.orgIdIsBlank(); } + if (requestVO.isRateLimitEnabled()) { + if (requestVO.getRateLimit() <= 0) { + throw BadRequestException.rateLimitIsInvalid(); + } + } else { + requestVO.setRateLimit(0); + } + Consumer createdConsumer = consumerService.createConsumer(convertToConsumer(requestVO)); if (Objects.isNull(expires)) { expires = DEFAULT_EXPIRES; } - ConsumerToken consumerToken = consumerService.generateAndSaveConsumerToken(createdConsumer, expires); + ConsumerToken consumerToken = consumerService.generateAndSaveConsumerToken(createdConsumer, requestVO.getRateLimit(), expires); if (requestVO.isAllowCreateApplication()) { consumerService.assignCreateApplicationRoleToConsumer(consumerToken.getToken()); } @@ -127,7 +135,7 @@ public List assignNamespaceRoleToConsumer( if (StringUtils.isEmpty(namespaceName)) { throw new BadRequestException("Params(NamespaceName) can not be empty."); } - if (null != envs){ + if (null != envs) { String[] envArray = envs.split(","); List envList = Lists.newArrayList(); // validate env parameter @@ -156,7 +164,7 @@ public List assignNamespaceRoleToConsumer( @GetMapping("/consumers") @PreAuthorize(value = "@permissionValidator.isSuperAdmin()") - public List getConsumerList(Pageable page){ + public List getConsumerList(Pageable page) { return consumerService.findConsumerInfoList(page); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java index 62bd2406fd2..65ad8e21454 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java @@ -26,6 +26,8 @@ public class ConsumerCreateRequestVO { private String orgId; private String orgName; private String ownerName; + private boolean rateLimitEnabled; + private int rateLimit; public String getAppId() { return appId; @@ -75,4 +77,20 @@ public void setOwnerName(String ownerName) { this.ownerName = ownerName; } + public boolean isRateLimitEnabled() { + return rateLimitEnabled; + } + + public void setRateLimitEnabled(boolean rateLimitEnabled) { + this.rateLimitEnabled = rateLimitEnabled; + } + + public int getRateLimit() { + return rateLimit; + } + + public void setRateLimit(int rateLimit) { + this.rateLimit = rateLimit; + } + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java index f6779da140c..cb6c7a0cf62 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java @@ -33,6 +33,8 @@ public class ConsumerInfo { private String token; private boolean allowCreateApplication; + private Integer rateLimit; + public String getAppId() { return appId; } @@ -104,4 +106,13 @@ public boolean isAllowCreateApplication() { public void setAllowCreateApplication(boolean allowCreateApplication) { this.allowCreateApplication = allowCreateApplication; } + + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(Integer rateLimit) { + this.rateLimit = rateLimit; + } + } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java index 66414b3eef2..38a662c6dae 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java @@ -28,8 +28,8 @@ public class AuthFilterConfiguration { @Bean public FilterRegistrationBean openApiAuthenticationFilter( - ConsumerAuthUtil consumerAuthUtil, - ConsumerAuditUtil consumerAuditUtil) { + ConsumerAuthUtil consumerAuthUtil, + ConsumerAuditUtil consumerAuditUtil) { FilterRegistrationBean openApiFilter = new FilterRegistrationBean<>(); @@ -39,5 +39,4 @@ public FilterRegistrationBean openApiAuthenticatio return openApiFilter; } - } diff --git a/apollo-portal/src/main/resources/static/i18n/en.json b/apollo-portal/src/main/resources/static/i18n/en.json index 7cab764e0c6..6be0df9228f 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -655,6 +655,12 @@ "Open.Manage.Consumer.AllowCreateApplicationTips": "(Allow third-party applications to create apps and grant them app administrator privileges.", "Open.Manage.Consumer.AllowCreateApplication.No": "no", "Open.Manage.Consumer.AllowCreateApplication.Yes": "yes", + "Open.Manage.Consumer.RateLimit.Enabled": "Whether to enable rate limit", + "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(After enabling this feature, when third-party applications publish configurations on Apollo, their traffic will be controlled according to the configured QPS limit)", + "Open.Manage.Consumer.RateLimitValue": "Rate limiting QPS", + "Open.Manage.Consumer.RateLimitValueTips": "(Unit: times/second, for example: 100 means that the configuration is published at most 100 times per second)", + "Open.Manage.Consumer.RateLimitValue.Error": "The minimum rate limiting QPS is 1", + "Open.Manage.Consumer.RateLimitValue.Display": "Unlimited", "Namespace.Role.Title": "Permission Management", "Namespace.Role.GrantModifyTo": "Permission to edit", "Namespace.Role.GrantModifyTo2": "(Can edit the configuration)", diff --git a/apollo-portal/src/main/resources/static/i18n/zh-CN.json b/apollo-portal/src/main/resources/static/i18n/zh-CN.json index 8c00fee807f..d3706b41e9e 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -655,6 +655,12 @@ "Open.Manage.Consumer.AllowCreateApplicationTips": "(允许第三方应用创建app,并且对创建出的app,拥有应用管理员的权限)", "Open.Manage.Consumer.AllowCreateApplication.No": "否", "Open.Manage.Consumer.AllowCreateApplication.Yes": "是", + "Open.Manage.Consumer.RateLimit.Enabled": "是否启用限流", + "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(开启后,第三方应用在 Apollo 上发布配置时,会根据配置的 QPS 限制,控制其流量)", + "Open.Manage.Consumer.RateLimitValue": "限流QPS", + "Open.Manage.Consumer.RateLimitValueTips": "(单位:次/秒,例如: 100 表示每秒最多发布 100 次配置)", + "Open.Manage.Consumer.RateLimitValue.Error": "限流QPS最小为1", + "Open.Manage.Consumer.RateLimitValue.Display": "无限制", "Namespace.Role.Title": "权限管理", "Namespace.Role.GrantModifyTo": "修改权", "Namespace.Role.GrantModifyTo2": "(可以修改配置)", diff --git a/apollo-portal/src/main/resources/static/open/add-consumer.html b/apollo-portal/src/main/resources/static/open/add-consumer.html index 2bbb4f3aa5b..94c490c2b35 100644 --- a/apollo-portal/src/main/resources/static/open/add-consumer.html +++ b/apollo-portal/src/main/resources/static/open/add-consumer.html @@ -78,6 +78,33 @@
{{'Open.Manage.CreateThirdApp' | translate }} +
+ +
+ + {{ 'Open.Manage.Consumer.RateLimit.Enabled.Tips' | translate }} +
+
+ +
+ +
+ + {{'Open.Manage.Consumer.RateLimitValueTips' | translate }} +
+
+
- - {{ consumer.orgName + '(' + consumer.orgId + ')' }} + + {{ consumer.rateLimit && consumer.rateLimit > 0 ? consumer.rateLimit : 'Open.Manage.Consumer.RateLimitValue.Display' | translate }} + + {{ consumer.orgName + '(' + consumer.orgId + ')' }} {{ consumer.ownerName }}/{{ consumer.ownerEmail }}