From 1b3e122313f3a9678d3bebc7387944f71bd21f9c Mon Sep 17 00:00:00 2001 From: yangzl Date: Sun, 3 Nov 2024 21:33:21 +0800 Subject: [PATCH 01/10] feat(portal): Add current limiting function to ConsumerToken --- .../apollo/openapi/entity/ConsumerToken.java | 12 ++ .../filter/ConsumerAuthenticationFilter.java | 49 +++++++- .../openapi/service/ConsumerService.java | 14 ++- .../apollo/openapi/util/ConsumerAuthUtil.java | 5 + .../portal/component/config/PortalConfig.java | 8 ++ .../AuthFilterConfiguration.java | 8 +- .../ConsumerAuthenticationFilterTest.java | 112 +++++++++++++++++- .../profiles/h2-default/apolloportaldb.sql | 1 + .../v230-v240/apolloportaldb-v230-v240.sql | 43 +++++++ .../apolloportaldb.sql | 1 + .../v230-v240/apolloportaldb-v230-v240.sql | 39 ++++++ .../profiles/mysql-default/apolloportaldb.sql | 1 + .../v230-v240/apolloportaldb-v230-v240.sql | 41 +++++++ scripts/sql/src/apolloportaldb.sql | 1 + .../v230-v240/apolloportaldb-v230-v240.sql | 25 ++++ 15 files changed, 347 insertions(+), 13 deletions(-) create mode 100644 scripts/sql/profiles/h2-default/delta/v230-v240/apolloportaldb-v230-v240.sql create mode 100644 scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloportaldb-v230-v240.sql create mode 100644 scripts/sql/profiles/mysql-default/delta/v230-v240/apolloportaldb-v230-v240.sql create mode 100644 scripts/sql/src/delta/v230-v240/apolloportaldb-v230-v240.sql 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..ce4a8fa2396 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 @@ -41,6 +41,9 @@ public class ConsumerToken extends BaseEntity { @Column(name = "`Token`", nullable = false) private String token; + @Column(name = "LimitCount") + private Integer limitCount; + @Column(name = "`Expires`", nullable = false) private Date expires; @@ -60,6 +63,14 @@ public void setToken(String token) { this.token = token; } + public Integer getLimitCount() { + return limitCount; + } + + public void setLimitCount(Integer limitCount) { + this.limitCount = limitCount; + } + public Date getExpires() { return expires; } @@ -71,6 +82,7 @@ public void setExpires(Date expires) { @Override public String toString() { return toStringHelper().add("consumerId", consumerId).add("token", token) + .add("limitCount", limitCount) .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..dff359e4a81 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,9 +16,13 @@ */ 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.ctrip.framework.apollo.portal.component.config.PortalConfig; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.RateLimiter; import java.io.IOException; import javax.servlet.Filter; @@ -29,18 +33,29 @@ 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 final PortalConfig portalConfig; - public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) { + private static final Cache> LIMITER = CacheBuilder.newBuilder().build(); + private static final int WARMUP_MILLIS = 1000; // ms + + public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil, PortalConfig portalConfig) { this.consumerAuthUtil = consumerAuthUtil; this.consumerAuditUtil = consumerAuditUtil; + this.portalConfig = portalConfig; } @Override @@ -55,14 +70,28 @@ 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 || consumerToken.getConsumerId() <= 0) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); return; } + Integer limitCount = consumerToken.getLimitCount(); + if (portalConfig.isOpenApiLimitEnabled() && limitCount > 0) { + try { + ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(token, limitCount); + long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS; + if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Too many call requests, the flow is limited"); + return; + } + } catch (Exception e) { + logger.error("ConsumerAuthenticationFilter ratelimit error", e); + } + } + + long consumerId = consumerToken.getConsumerId(); consumerAuthUtil.storeConsumerId(request, consumerId); consumerAuditUtil.audit(request, consumerId); @@ -73,4 +102,14 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain public void destroy() { //nothing } + + private ImmutablePair getOrCreateRateLimiterPair(String key, Integer limitCount) { + ImmutablePair rateLimiterPair = LIMITER.getIfPresent(key); + if (rateLimiterPair == null) { + rateLimiterPair = ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount)); + LIMITER.put(key, rateLimiterPair); + } + return rateLimiterPair; + } + } 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..becaf48a7b2 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 @@ -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(); } @@ -311,7 +314,9 @@ public void createConsumerAudits(Iterable consumerAudits) { @Transactional public ConsumerToken createConsumerToken(ConsumerToken entity) { entity.setId(0); //for protection - + if (entity.getLimitCount() <= 0) { + entity.setLimitCount(portalConfig.openApiLimitCount()); + } return consumerTokenRepository.save(entity); } @@ -322,6 +327,7 @@ private ConsumerToken generateConsumerToken(Consumer consumer, Date expires) { ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setConsumerId(consumerId); + consumerToken.setLimitCount(portalConfig.openApiLimitCount()); consumerToken.setExpires(expires); consumerToken.setDataChangeCreatedBy(createdBy); consumerToken.setDataChangeCreatedTime(createdTime); 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/component/config/PortalConfig.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java index 982515be490..f95be758038 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java @@ -242,6 +242,14 @@ public String consumerTokenSalt() { return getValue("consumer.token.salt", "apollo-portal"); } + public int openApiLimitCount() { + return getIntProperty("open.api.limit.count", 20); + } + + public boolean isOpenApiLimitEnabled() { + return getBooleanProperty("open.api.limit.enabled", false); + } + public boolean isEmailEnabled() { return getBooleanProperty("email.enabled", false); } 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..40eb0309029 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 @@ -19,6 +19,7 @@ import com.ctrip.framework.apollo.openapi.filter.ConsumerAuthenticationFilter; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; +import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,12 +29,13 @@ public class AuthFilterConfiguration { @Bean public FilterRegistrationBean openApiAuthenticationFilter( - ConsumerAuthUtil consumerAuthUtil, - ConsumerAuditUtil consumerAuditUtil) { + ConsumerAuthUtil consumerAuthUtil, + ConsumerAuditUtil consumerAuditUtil, + PortalConfig portalConfig) { FilterRegistrationBean openApiFilter = new FilterRegistrationBean<>(); - openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil)); + openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig)); openApiFilter.addUrlPatterns("/openapi/*"); return openApiFilter; diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java index 876562849bb..dcdaaec2f86 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java @@ -16,9 +16,16 @@ */ 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.ctrip.framework.apollo.portal.component.config.PortalConfig; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,6 +40,9 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -48,6 +58,9 @@ public class ConsumerAuthenticationFilterTest { private ConsumerAuthUtil consumerAuthUtil; @Mock private ConsumerAuditUtil consumerAuditUtil; + @Mock + private PortalConfig portalConfig; + @Mock private HttpServletRequest request; @Mock @@ -57,7 +70,7 @@ public class ConsumerAuthenticationFilterTest { @Before public void setUp() throws Exception { - authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil); + authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig); } @Test @@ -89,4 +102,101 @@ public void testAuthFailed() throws Exception { verify(consumerAuditUtil, never()).audit(eq(request), anyLong()); verify(filterChain, never()).doFilter(request, response); } + + + @Test + public void testRateLimitSuccessfully() throws Exception { + String someToken = "someToken"; + Long someConsumerId = 1L; + int qps = 5; + int durationInSeconds = 10; + + setupRateLimitMocks(someToken, someConsumerId, qps); + + Runnable task = () -> { + try { + authenticationFilter.doFilter(request, response, filterChain); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (ServletException e) { + throw new RuntimeException(e); + } + }; + + executeWithQps(qps, task, durationInSeconds); + + int total = qps * durationInSeconds; + + verify(consumerAuthUtil, times(total)).storeConsumerId(request, someConsumerId); + verify(consumerAuditUtil, times(total)).audit(request, someConsumerId); + verify(filterChain, times(total)).doFilter(request, response); + + } + + + @Test + public void testRateLimitPartFailure() throws Exception { + String someToken = "someToken"; + Long someConsumerId = 1L; + int qps = 5; + int durationInSeconds = 10; + + setupRateLimitMocks(someToken, someConsumerId, qps); + + Runnable task = () -> { + try { + authenticationFilter.doFilter(request, response, filterChain); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (ServletException e) { + throw new RuntimeException(e); + } + }; + + executeWithQps(qps + 1, task, durationInSeconds); + + int leastTimes = qps * durationInSeconds; + int mostTimes = (qps + 1) * durationInSeconds; + + verify(response, atLeastOnce()).sendError(eq(HttpServletResponse.SC_FORBIDDEN), anyString()); + + verify(consumerAuthUtil, atLeast(leastTimes)).storeConsumerId(request, someConsumerId); + verify(consumerAuthUtil, atMost(mostTimes)).storeConsumerId(request, someConsumerId); + verify(consumerAuditUtil, atLeast(leastTimes)).audit(request, someConsumerId); + verify(consumerAuditUtil, atMost(mostTimes)).audit(request, someConsumerId); + verify(filterChain, atLeast(leastTimes)).doFilter(request, response); + verify(filterChain, atMost(mostTimes)).doFilter(request, response); + + } + + + private void setupRateLimitMocks(String someToken, Long someConsumerId, int qps) { + ConsumerToken someConsumerToken = new ConsumerToken(); + someConsumerToken.setConsumerId(someConsumerId); + someConsumerToken.setLimitCount(qps); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); + when(consumerAuthUtil.getConsumerId(someToken)).thenReturn(someConsumerId); + when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); + when(portalConfig.isOpenApiLimitEnabled()).thenReturn(true); + } + + + public static void executeWithQps(int qps, Runnable task, int durationInSeconds) { + ExecutorService executor = Executors.newFixedThreadPool(qps); + long totalTasks = qps * durationInSeconds; + + for (int i = 0; i < totalTasks; i++) { + executor.submit(task); + try { + TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + executor.shutdown(); + } + } diff --git a/scripts/sql/profiles/h2-default/apolloportaldb.sql b/scripts/sql/profiles/h2-default/apolloportaldb.sql index e1be67d3d6e..4ebf711c343 100644 --- a/scripts/sql/profiles/h2-default/apolloportaldb.sql +++ b/scripts/sql/profiles/h2-default/apolloportaldb.sql @@ -161,6 +161,7 @@ CREATE TABLE `ConsumerToken` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId', `Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token', + `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值', `Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间', `IsDeleted` boolean NOT NULL DEFAULT FALSE COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/h2-default/delta/v230-v240/apolloportaldb-v230-v240.sql b/scripts/sql/profiles/h2-default/delta/v230-v240/apolloportaldb-v230-v240.sql new file mode 100644 index 00000000000..7bb1675a1f5 --- /dev/null +++ b/scripts/sql/profiles/h2-default/delta/v230-v240/apolloportaldb-v230-v240.sql @@ -0,0 +1,43 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo portal db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- + +-- H2 Function +-- ------------------------------------------------------------ +CREATE ALIAS IF NOT EXISTS UNIX_TIMESTAMP FOR "com.ctrip.framework.apollo.common.jpa.H2Function.unixTimestamp"; + +-- + +ALTER TABLE `ConsumerToken` ADD COLUMN `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值' AFTER `Token`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/profiles/mysql-database-not-specified/apolloportaldb.sql b/scripts/sql/profiles/mysql-database-not-specified/apolloportaldb.sql index e6de5a1a4a9..2649d658a75 100644 --- a/scripts/sql/profiles/mysql-database-not-specified/apolloportaldb.sql +++ b/scripts/sql/profiles/mysql-database-not-specified/apolloportaldb.sql @@ -162,6 +162,7 @@ CREATE TABLE `ConsumerToken` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId', `Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token', + `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值', `Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloportaldb-v230-v240.sql b/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloportaldb-v230-v240.sql new file mode 100644 index 00000000000..e3dbbbea4e3 --- /dev/null +++ b/scripts/sql/profiles/mysql-database-not-specified/delta/v230-v240/apolloportaldb-v230-v240.sql @@ -0,0 +1,39 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo portal db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- +-- + +ALTER TABLE `ConsumerToken` + ADD COLUMN `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值' AFTER `Token`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/profiles/mysql-default/apolloportaldb.sql b/scripts/sql/profiles/mysql-default/apolloportaldb.sql index 264f948a53b..a0b01c9c413 100644 --- a/scripts/sql/profiles/mysql-default/apolloportaldb.sql +++ b/scripts/sql/profiles/mysql-default/apolloportaldb.sql @@ -167,6 +167,7 @@ CREATE TABLE `ConsumerToken` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId', `Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token', + `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值', `Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloportaldb-v230-v240.sql b/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloportaldb-v230-v240.sql new file mode 100644 index 00000000000..51fcbd5f7ed --- /dev/null +++ b/scripts/sql/profiles/mysql-default/delta/v230-v240/apolloportaldb-v230-v240.sql @@ -0,0 +1,41 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo portal db from v2.3.0 to v2.4.0 + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== +-- +-- +-- Use Database +Use ApolloPortalDB; + +ALTER TABLE `ConsumerToken` + ADD COLUMN `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值' AFTER `Token`; + +-- +-- =============================================================================== +-- == == +-- == Generated from 'scripts/sql/src/' == +-- == by running 'mvn compile -pl apollo-build-sql-converter -Psql-converter'. == +-- == DO NOT EDIT !!! == +-- == == +-- =============================================================================== diff --git a/scripts/sql/src/apolloportaldb.sql b/scripts/sql/src/apolloportaldb.sql index 323a8191149..3398e5db88e 100644 --- a/scripts/sql/src/apolloportaldb.sql +++ b/scripts/sql/src/apolloportaldb.sql @@ -155,6 +155,7 @@ CREATE TABLE `ConsumerToken` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId', `Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token', + `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值', `Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds', diff --git a/scripts/sql/src/delta/v230-v240/apolloportaldb-v230-v240.sql b/scripts/sql/src/delta/v230-v240/apolloportaldb-v230-v240.sql new file mode 100644 index 00000000000..b7fbb33ba0d --- /dev/null +++ b/scripts/sql/src/delta/v230-v240/apolloportaldb-v230-v240.sql @@ -0,0 +1,25 @@ +-- +-- Copyright 2024 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- delta schema to upgrade apollo portal db from v2.3.0 to v2.4.0 + +-- ${gists.autoGeneratedDeclaration} +-- ${gists.h2Function} +-- ${gists.useDatabase} + +ALTER TABLE `ConsumerToken` + ADD COLUMN `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值' AFTER `Token`; + +-- ${gists.autoGeneratedDeclaration} From 1bae71f865a1b845675ac967bdd71fcaf1e4a535 Mon Sep 17 00:00:00 2001 From: yangzl Date: Sun, 3 Nov 2024 22:59:15 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix=EF=BC=9Aadd=20CHANGES.md=20and=20opti?= =?UTF-8?q?mize=20some=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + .../filter/ConsumerAuthenticationFilter.java | 13 +++++++++++-- .../apollo/SkipAuthorizationConfiguration.java | 6 ++++++ .../filter/ConsumerAuthenticationFilterTest.java | 8 +++++--- docs/en/deployment/distributed-deployment-guide.md | 12 ++++++++++++ docs/zh/deployment/distributed-deployment-guide.md | 11 +++++++++++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 48d7b9967b0..88a2833ca9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ Apollo 2.4.0 * [Feature: openapi query namespace support not fill item](https://github.com/apolloconfig/apollo/pull/5249) * [Refactor: align database ClusterName and NamespaceName fields lengths](https://github.com/apolloconfig/apollo/pull/5263) * [Feature: Added the value length limit function for AppId-level configuration items](https://github.com/apolloconfig/apollo/pull/5264) +* [Feature: Added current 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-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 dff359e4a81..2db7d11afbd 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 @@ -24,7 +24,7 @@ import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.RateLimiter; import java.io.IOException; - +import java.util.concurrent.TimeUnit; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -49,8 +49,12 @@ public class ConsumerAuthenticationFilter implements Filter { private final ConsumerAuditUtil consumerAuditUtil; private final PortalConfig portalConfig; - private static final Cache> LIMITER = CacheBuilder.newBuilder().build(); private static final int WARMUP_MILLIS = 1000; // ms + private static final int RATE_LIMITER_CACHE_MAX_SIZE = 50000; + + private static final Cache> LIMITER = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.DAYS) + .maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build(); public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil, PortalConfig portalConfig) { this.consumerAuthUtil = consumerAuthUtil; @@ -78,6 +82,9 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain } Integer limitCount = consumerToken.getLimitCount(); + if (limitCount == null) { + limitCount = 0; + } if (portalConfig.isOpenApiLimitEnabled() && limitCount > 0) { try { ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(token, limitCount); @@ -88,6 +95,8 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain } } catch (Exception e) { logger.error("ConsumerAuthenticationFilter ratelimit error", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Rate limiting failed"); + return; } } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java index fa4e166b7a1..e767ea5d547 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java @@ -17,6 +17,7 @@ package com.ctrip.framework.apollo; import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; +import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.component.PermissionValidator; import org.springframework.context.annotation.Bean; @@ -50,6 +51,11 @@ public ConsumerPermissionValidator consumerPermissionValidator() { public ConsumerAuthUtil consumerAuthUtil() { final ConsumerAuthUtil mock = mock(ConsumerAuthUtil.class); when(mock.getConsumerId(any())).thenReturn(1L); + + ConsumerToken someConsumerToken = new ConsumerToken(); + someConsumerToken.setConsumerId(1L); + someConsumerToken.setLimitCount(20); + when(mock.getConsumerToken(any())).thenReturn(someConsumerToken); return mock; } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java index dcdaaec2f86..e9d8e16071c 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java @@ -78,8 +78,11 @@ public void testAuthSuccessfully() throws Exception { String someToken = "someToken"; Long someConsumerId = 1L; + ConsumerToken someConsumerToken = new ConsumerToken(); + someConsumerToken.setConsumerId(someConsumerId); + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); - when(consumerAuthUtil.getConsumerId(someToken)).thenReturn(someConsumerId); + when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); authenticationFilter.doFilter(request, response, filterChain); @@ -93,7 +96,7 @@ public void testAuthFailed() throws Exception { String someInvalidToken = "someInvalidToken"; when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someInvalidToken); - when(consumerAuthUtil.getConsumerId(someInvalidToken)).thenReturn(null); + when(consumerAuthUtil.getConsumerToken(someInvalidToken)).thenReturn(null); authenticationFilter.doFilter(request, response, filterChain); @@ -176,7 +179,6 @@ private void setupRateLimitMocks(String someToken, Long someConsumerId, int qps) someConsumerToken.setLimitCount(qps); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); - when(consumerAuthUtil.getConsumerId(someToken)).thenReturn(someConsumerId); when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); when(portalConfig.isOpenApiLimitEnabled()).thenReturn(true); } diff --git a/docs/en/deployment/distributed-deployment-guide.md b/docs/en/deployment/distributed-deployment-guide.md index 91afbe7a4fb..9028ec2fd22 100644 --- a/docs/en/deployment/distributed-deployment-guide.md +++ b/docs/en/deployment/distributed-deployment-guide.md @@ -1455,6 +1455,18 @@ Default is 200, which means that each environment will return up to 200 results Modifying this parameter may affect the performance of the search function, so before modifying it, you should conduct sufficient testing and adjust the value of `apollo.portal.search.perEnvMaxResults` appropriately according to the actual business requirements and system resources to balance the performance and the number of search results. +### 3.1.15 open.api.limit.count - Default value of ConsumerToken current limit value + +> For versions 2.4.0 and above + +The default value is 20. When creating a third-party application in the Open Platform Authorization Management, the default current limit value of the created ConsumerToken is this value. + +### 3.1.16 open.api.limit.enabled - Whether to enable the ConsumerToken current limiting function + +> For versions 2.4.0 and above + +The default value is `false`. When set to `true`, it means that each ConsumerToken request to the Apollo OpenAPI interface will be limited to a specific QPS, and the current limit value is the value of the `limitCount` field in the `ConsumerToken` table. + ## 3.2 Adjusting ApolloConfigDB configuration Configuration items are uniformly stored in the ApolloConfigDB.ServerConfig table. It should be noted that each environment's ApolloConfigDB.ServerConfig needs to be configured separately, and the modification takes effect in real time for one minute afterwards. diff --git a/docs/zh/deployment/distributed-deployment-guide.md b/docs/zh/deployment/distributed-deployment-guide.md index 89473a8a0c2..e948ee88527 100644 --- a/docs/zh/deployment/distributed-deployment-guide.md +++ b/docs/zh/deployment/distributed-deployment-guide.md @@ -1400,6 +1400,17 @@ portal上“帮助”链接的地址,默认是Apollo github的wiki首页,可 修改该参数可能会影响搜索功能的性能,因此在修改之前应该进行充分的测试,根据实际业务需求和系统资源情况,适当调整`apollo.portal.search.perEnvMaxResults`的值,以平衡性能和搜索结果的数量 +### 3.1.15 open.api.limit.count - ConsumerToken 限流值的默认值 + +> 适用于2.4.0及以上版本 + +默认为20,当在 `开放平台授权管理-创建第三方应用` 时,创建的 ConsumerToken 默认的限流值即为此值 + +### 3.1.16 open.api.limit.enabled - 是否开启 ConsumerToken 限流功能 + +> 适用于2.4.0及以上版本 + +默认为`false`,当设置为`true`,意味着每个 ConsumerToken 请求Apollo OpenAPI 接口的时候,都会被限制到特定的QPS,限流值为`ConsumerToken`表中的`limitCount`字段的值 ## 3.2 调整ApolloConfigDB配置 配置项统一存储在ApolloConfigDB.ServerConfig表中,需要注意每个环境的ApolloConfigDB.ServerConfig都需要单独配置,修改完一分钟实时生效。 From 826ea74b766ac13346a5b27bc598b620d711330c Mon Sep 17 00:00:00 2001 From: yangzl Date: Sun, 17 Nov 2024 23:25:25 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat(openapi):=20=E9=87=8D=E6=9E=84=20Con?= =?UTF-8?q?sumerToken=20=E9=99=90=E6=B5=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/BadRequestException.java | 4 +++ .../apollo/openapi/entity/ConsumerToken.java | 16 +++++----- .../filter/ConsumerAuthenticationFilter.java | 26 ++++++++--------- .../openapi/service/ConsumerService.java | 11 +++---- .../portal/component/config/PortalConfig.java | 8 ----- .../portal/controller/ConsumerController.java | 14 +++++++-- .../vo/consumer/ConsumerCreateRequestVO.java | 18 ++++++++++++ .../src/main/resources/static/i18n/en.json | 5 ++++ .../src/main/resources/static/i18n/zh-CN.json | 5 ++++ .../resources/static/open/add-consumer.html | 29 ++++++++++++++++++- .../controller/open/OpenManageController.js | 16 ++++++++++ .../SkipAuthorizationConfiguration.java | 2 +- .../ConsumerAuthenticationFilterTest.java | 8 +++-- .../controller/NamespaceControllerTest.java | 13 +++++++-- .../controller/ConsumerControllerTest.java | 8 ++--- ...sumerServiceIntegrationTest.commonData.sql | 4 +-- ...t.testFindAppIdsAuthorizedByConsumerId.sql | 4 +-- ...eControllerTest.testCreateAppNamespace.sql | 4 +-- .../distributed-deployment-guide.md | 12 -------- .../distributed-deployment-guide.md | 12 -------- .../profiles/h2-default/apolloportaldb.sql | 2 +- .../v230-v240/apolloportaldb-v230-v240.sql | 2 +- .../apolloportaldb.sql | 2 +- .../v230-v240/apolloportaldb-v230-v240.sql | 2 +- .../profiles/mysql-default/apolloportaldb.sql | 2 +- .../v230-v240/apolloportaldb-v230-v240.sql | 2 +- scripts/sql/src/apolloportaldb.sql | 2 +- .../v230-v240/apolloportaldb-v230-v240.sql | 2 +- 28 files changed, 148 insertions(+), 87 deletions(-) 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..2aeb4c3e0f3 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("Ratelimit 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 ce4a8fa2396..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,8 +42,9 @@ public class ConsumerToken extends BaseEntity { @Column(name = "`Token`", nullable = false) private String token; - @Column(name = "LimitCount") - private Integer limitCount; + @PositiveOrZero + @Column(name = "`RateLimit`", nullable = false) + private Integer rateLimit; @Column(name = "`Expires`", nullable = false) private Date expires; @@ -63,12 +65,12 @@ public void setToken(String token) { this.token = token; } - public Integer getLimitCount() { - return limitCount; + public Integer getRateLimit() { + return rateLimit; } - public void setLimitCount(Integer limitCount) { - this.limitCount = limitCount; + public void setRateLimit(Integer rateLimit) { + this.rateLimit = rateLimit; } public Date getExpires() { @@ -82,7 +84,7 @@ public void setExpires(Date expires) { @Override public String toString() { return toStringHelper().add("consumerId", consumerId).add("token", token) - .add("limitCount", limitCount) + .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 2db7d11afbd..3e1ae49ae59 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 @@ -24,6 +24,7 @@ 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; @@ -52,6 +53,8 @@ public class ConsumerAuthenticationFilter implements Filter { private static final int WARMUP_MILLIS = 1000; // ms private static final int RATE_LIMITER_CACHE_MAX_SIZE = 50000; + private static final int TOO_MANY_REQUESTS = 429; + private static final Cache> LIMITER = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.DAYS) .maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build(); @@ -76,21 +79,18 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain String token = request.getHeader(HttpHeaders.AUTHORIZATION); ConsumerToken consumerToken = consumerAuthUtil.getConsumerToken(token); - if (null == consumerToken || consumerToken.getConsumerId() <= 0) { + if (null == consumerToken) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); return; } - Integer limitCount = consumerToken.getLimitCount(); - if (limitCount == null) { - limitCount = 0; - } - if (portalConfig.isOpenApiLimitEnabled() && limitCount > 0) { + Integer rateLimit = consumerToken.getRateLimit(); + if (null != rateLimit && rateLimit > 0) { try { - ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(token, limitCount); + ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(token, rateLimit); long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS; if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Too many call requests, the flow is limited"); + response.sendError(TOO_MANY_REQUESTS, "Too Many Requests, the flow is limited"); return; } } catch (Exception e) { @@ -113,12 +113,12 @@ public void destroy() { } private ImmutablePair getOrCreateRateLimiterPair(String key, Integer limitCount) { - ImmutablePair rateLimiterPair = LIMITER.getIfPresent(key); - if (rateLimiterPair == null) { - rateLimiterPair = ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount)); - LIMITER.put(key, rateLimiterPair); + try{ + return LIMITER.get(key, () -> + ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount))); + } catch (ExecutionException e) { + throw new RuntimeException("Failed to create rate limiter", e); } - return rateLimiterPair; } } 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 becaf48a7b2..ab5d25e65ae 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 @@ -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); consumerToken.setId(0); return consumerTokenRepository.save(consumerToken); @@ -314,20 +314,17 @@ public void createConsumerAudits(Iterable consumerAudits) { @Transactional public ConsumerToken createConsumerToken(ConsumerToken entity) { entity.setId(0); //for protection - if (entity.getLimitCount() <= 0) { - entity.setLimitCount(portalConfig.openApiLimitCount()); - } 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(); ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setConsumerId(consumerId); - consumerToken.setLimitCount(portalConfig.openApiLimitCount()); + consumerToken.setRateLimit(rateLimit); consumerToken.setExpires(expires); consumerToken.setDataChangeCreatedBy(createdBy); consumerToken.setDataChangeCreatedTime(createdTime); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java index f95be758038..982515be490 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java @@ -242,14 +242,6 @@ public String consumerTokenSalt() { return getValue("consumer.token.salt", "apollo-portal"); } - public int openApiLimitCount() { - return getIntProperty("open.api.limit.count", 20); - } - - public boolean isOpenApiLimitEnabled() { - return getBooleanProperty("open.api.limit.enabled", false); - } - public boolean isEmailEnabled() { return getBooleanProperty("email.enabled", false); } 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..1e695b3159e 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.isRateLimitEenabled()) { + 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..248f8312516 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 rateLimitEenabled; + private int rateLimit; public String getAppId() { return appId; @@ -75,4 +77,20 @@ public void setOwnerName(String ownerName) { this.ownerName = ownerName; } + public boolean isRateLimitEenabled() { + return rateLimitEenabled; + } + + public void setRateLimitEenabled(boolean rateLimitEenabled) { + this.rateLimitEenabled = rateLimitEenabled; + } + + public int getRateLimit() { + return rateLimit; + } + + public void setRateLimit(int rateLimit) { + this.rateLimit = rateLimit; + } + } diff --git a/apollo-portal/src/main/resources/static/i18n/en.json b/apollo-portal/src/main/resources/static/i18n/en.json index 69ae8e5e4cf..88d8b4ba91a 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -654,6 +654,11 @@ "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 current 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": "Current 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 current limiting QPS is 1", "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 8ae676eda59..3716457476d 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -654,6 +654,11 @@ "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", "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..14d066ad812 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 }} +
+
+
-
+
diff --git a/apollo-portal/src/main/resources/static/scripts/controller/open/OpenManageController.js b/apollo-portal/src/main/resources/static/scripts/controller/open/OpenManageController.js index 4eb13e7b1d7..dc1c9efc7a6 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/open/OpenManageController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/open/OpenManageController.js @@ -41,8 +41,8 @@ function OpenManageController($scope, $translate, toastr, AppUtil, OrganizationS $scope.preDeleteConsumer = preDeleteConsumer; $scope.deleteConsumer = deleteConsumer; $scope.preGrantPermission = preGrantPermission; - $scope.toggleRateLimitEenabledInput = function() { - if (!$scope.consumer.rateLimitEenabled) { + $scope.toggleRateLimitEnabledInput = function() { + if (!$scope.consumer.rateLimitEnabled) { $scope.consumer.rateLimit = 0; } }; @@ -169,7 +169,7 @@ function OpenManageController($scope, $translate, toastr, AppUtil, OrganizationS return; } - if ($scope.consumer.rateLimitEenabled) { + if ($scope.consumer.rateLimitEnabled) { if ($scope.consumer.rateLimit < 1) { toastr.warning($translate.instant('Open.Manage.Consumer.RateLimitValue.Error')); $scope.submitBtnDisabled = false; diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java index 1eb836431ef..d7eb02d47da 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java @@ -39,7 +39,7 @@ public class NamespaceControllerTest extends AbstractControllerTest { static final HttpHeaders HTTP_HEADERS_WITH_TOKEN = new HttpHeaders() {{ - set(HttpHeaders.AUTHORIZATION, "3c16bf5b1f44b465179253442460e8c0ad845289"); + set(HttpHeaders.AUTHORIZATION, "test-token"); }}; @Autowired private ConsumerPermissionValidator consumerPermissionValidator; From a622f565ed228333ec786897ffc5f387e0e4d98d Mon Sep 17 00:00:00 2001 From: yangzl Date: Thu, 21 Nov 2024 08:33:39 +0800 Subject: [PATCH 05/10] refactor(openapi): Refactor consumer authentication filters and related services --- .../filter/ConsumerAuthenticationFilter.java | 11 +++-------- .../apollo/openapi/service/ConsumerService.java | 1 - .../configuration/AuthFilterConfiguration.java | 7 ++----- .../src/main/resources/static/i18n/en.json | 6 +++--- .../apollo/SkipAuthorizationConfiguration.java | 1 + .../filter/ConsumerAuthenticationFilterTest.java | 15 +++++++-------- .../v1/controller/NamespaceControllerTest.java | 13 ++----------- .../portal/controller/ConsumerControllerTest.java | 2 +- 8 files changed, 19 insertions(+), 37 deletions(-) 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 3e1ae49ae59..1a7b1f909cf 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 @@ -19,13 +19,11 @@ 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.ctrip.framework.apollo.portal.component.config.PortalConfig; 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; @@ -48,21 +46,18 @@ public class ConsumerAuthenticationFilter implements Filter { private final ConsumerAuthUtil consumerAuthUtil; private final ConsumerAuditUtil consumerAuditUtil; - private final PortalConfig portalConfig; private static final int WARMUP_MILLIS = 1000; // ms - private static final int RATE_LIMITER_CACHE_MAX_SIZE = 50000; + 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() - .expireAfterWrite(1, TimeUnit.DAYS) .maximumSize(RATE_LIMITER_CACHE_MAX_SIZE).build(); - public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil, PortalConfig portalConfig) { + public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) { this.consumerAuthUtil = consumerAuthUtil; this.consumerAuditUtil = consumerAuditUtil; - this.portalConfig = portalConfig; } @Override @@ -87,7 +82,7 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain Integer rateLimit = consumerToken.getRateLimit(); if (null != rateLimit && rateLimit > 0) { try { - ImmutablePair rateLimiterPair = getOrCreateRateLimiterPair(token, rateLimit); + 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"); 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 cbc4b4b701a..f9b8ed5882c 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 @@ -122,7 +122,6 @@ public Consumer createConsumer(Consumer consumer) { public ConsumerToken generateAndSaveConsumerToken(Consumer consumer, Integer rateLimit, Date expires) { Preconditions.checkArgument(consumer != null, "Consumer can not be null"); - Preconditions.checkArgument(rateLimit != null && rateLimit >= 0, "Rate limit must be non-negative"); ConsumerToken consumerToken = generateConsumerToken(consumer, rateLimit, expires); consumerToken.setId(0); 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 40eb0309029..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 @@ -19,7 +19,6 @@ import com.ctrip.framework.apollo.openapi.filter.ConsumerAuthenticationFilter; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; -import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,16 +29,14 @@ public class AuthFilterConfiguration { @Bean public FilterRegistrationBean openApiAuthenticationFilter( ConsumerAuthUtil consumerAuthUtil, - ConsumerAuditUtil consumerAuditUtil, - PortalConfig portalConfig) { + ConsumerAuditUtil consumerAuditUtil) { FilterRegistrationBean openApiFilter = new FilterRegistrationBean<>(); - openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig)); + openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil)); openApiFilter.addUrlPatterns("/openapi/*"); 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 88d8b4ba91a..3e5ba047b2b 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -654,11 +654,11 @@ "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 current limit", + "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": "Current limiting QPS", + "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 current limiting QPS is 1", + "Open.Manage.Consumer.RateLimitValue.Error": "The minimum rate limiting QPS is 1", "Namespace.Role.Title": "Permission Management", "Namespace.Role.GrantModifyTo": "Permission to edit", "Namespace.Role.GrantModifyTo2": "(Can edit the configuration)", diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java index 017c80b8649..631f1b77b5b 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/SkipAuthorizationConfiguration.java @@ -54,6 +54,7 @@ public ConsumerAuthUtil consumerAuthUtil() { ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(1L); + someConsumerToken.setToken("some-token"); someConsumerToken.setRateLimit(20); when(mock.getConsumerToken(any())).thenReturn(someConsumerToken); return mock; diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java index afa79572838..3ed0fc3e628 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java @@ -20,7 +20,6 @@ import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; -import com.ctrip.framework.apollo.portal.component.config.PortalConfig; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -61,8 +60,6 @@ public class ConsumerAuthenticationFilterTest { private ConsumerAuthUtil consumerAuthUtil; @Mock private ConsumerAuditUtil consumerAuditUtil; - @Mock - private PortalConfig portalConfig; @Mock private HttpServletRequest request; @@ -73,7 +70,7 @@ public class ConsumerAuthenticationFilterTest { @Before public void setUp() throws Exception { - authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig); + authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil); } @Test @@ -115,7 +112,7 @@ public void testRateLimitSuccessfully() throws Exception { String someToken = "someToken"; Long someConsumerId = 1L; int qps = 5; - int durationInSeconds = 10; + int durationInSeconds = 3; setupRateLimitMocks(someToken, someConsumerId, qps); @@ -145,7 +142,7 @@ public void testRateLimitPartFailure() throws Exception { String someToken = "someToken"; Long someConsumerId = 1L; int qps = 5; - int durationInSeconds = 10; + int durationInSeconds = 3; setupRateLimitMocks(someToken, someConsumerId, qps); @@ -159,10 +156,11 @@ public void testRateLimitPartFailure() throws Exception { } }; - executeWithQps(qps + 1, task, durationInSeconds); + int realQps = qps + 10; + executeWithQps(realQps, task, durationInSeconds); int leastTimes = qps * durationInSeconds; - int mostTimes = (qps + 1) * durationInSeconds; + int mostTimes = realQps * durationInSeconds; verify(response, atLeastOnce()).sendError(eq(TOO_MANY_REQUESTS), anyString()); @@ -180,6 +178,7 @@ private void setupRateLimitMocks(String someToken, Long someConsumerId, int qps) ConsumerToken someConsumerToken = new ConsumerToken(); someConsumerToken.setConsumerId(someConsumerId); someConsumerToken.setRateLimit(qps); + someConsumerToken.setToken(someToken); when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken); when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken); diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java index d7eb02d47da..93c1b3a182e 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/NamespaceControllerTest.java @@ -23,9 +23,6 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.client.HttpClientErrorException; import static org.hamcrest.Matchers.containsString; @@ -37,10 +34,6 @@ */ @ActiveProfiles("skipAuthorization") public class NamespaceControllerTest extends AbstractControllerTest { - - static final HttpHeaders HTTP_HEADERS_WITH_TOKEN = new HttpHeaders() {{ - set(HttpHeaders.AUTHORIZATION, "test-token"); - }}; @Autowired private ConsumerPermissionValidator consumerPermissionValidator; @@ -54,11 +47,9 @@ public void shouldFailWhenAppNamespaceNameIsInvalid() { dto.setFormat(ConfigFileFormat.Properties.getValue()); dto.setDataChangeCreatedBy("apollo"); try { - restTemplate.exchange( + restTemplate.postForEntity( url("/openapi/v1/apps/{appId}/appnamespaces"), - HttpMethod.POST, - new HttpEntity<>(dto, HTTP_HEADERS_WITH_TOKEN), - OpenAppNamespaceDTO.class, dto.getAppId() + dto, OpenAppNamespaceDTO.class, dto.getAppId() ); Assert.fail("should throw"); } catch (HttpClientErrorException e) { diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java index ba4ef7d0cf7..ea6a525ad82 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java @@ -63,7 +63,7 @@ void createWithCompatibility() { Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)) - .generateAndSaveConsumerToken(Mockito.any(), Mockito.any(),Mockito.any()); + .generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.verify(consumerService, Mockito.times(0)) .assignCreateApplicationRoleToConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); From 4939d70c7bdc4078d8794f1a7d686bd6a98b0691 Mon Sep 17 00:00:00 2001 From: yangzl Date: Thu, 21 Nov 2024 23:09:13 +0800 Subject: [PATCH 06/10] test(apollo-portal): Optimize the rate limiting test of ConsumerAuthenticationFilter --- .../filter/ConsumerAuthenticationFilterTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java index 3ed0fc3e628..171242c2e6d 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/filter/ConsumerAuthenticationFilterTest.java @@ -109,7 +109,7 @@ public void testAuthFailed() throws Exception { @Test public void testRateLimitSuccessfully() throws Exception { - String someToken = "someToken"; + String someToken = "some-ratelimit-success-token"; Long someConsumerId = 1L; int qps = 5; int durationInSeconds = 3; @@ -126,9 +126,10 @@ public void testRateLimitSuccessfully() throws Exception { } }; - executeWithQps(qps, task, durationInSeconds); + int realQps = qps - 1; + executeWithQps(realQps, task, durationInSeconds); - int total = qps * durationInSeconds; + int total = realQps * durationInSeconds; verify(consumerAuthUtil, times(total)).storeConsumerId(request, someConsumerId); verify(consumerAuditUtil, times(total)).audit(request, someConsumerId); @@ -139,7 +140,7 @@ public void testRateLimitSuccessfully() throws Exception { @Test public void testRateLimitPartFailure() throws Exception { - String someToken = "someToken"; + String someToken = "some-ratelimit-fail-token"; Long someConsumerId = 1L; int qps = 5; int durationInSeconds = 3; @@ -156,7 +157,7 @@ public void testRateLimitPartFailure() throws Exception { } }; - int realQps = qps + 10; + int realQps = qps + 3; executeWithQps(realQps, task, durationInSeconds); int leastTimes = qps * durationInSeconds; From 22ebb4fb98456b1812fa6905a9096ba4066e5b72 Mon Sep 17 00:00:00 2001 From: yangzl Date: Sat, 23 Nov 2024 20:37:22 +0800 Subject: [PATCH 07/10] featapi(open): Updated management page to show consumer rate limit information --- .../filter/ConsumerAuthenticationFilter.java | 4 +++- .../openapi/service/ConsumerService.java | 24 ++++++++++++++++--- .../entity/vo/consumer/ConsumerInfo.java | 11 +++++++++ .../main/resources/static/open/manage.html | 8 ++++--- .../controller/open/OpenManageController.js | 2 +- 5 files changed, 41 insertions(+), 8 deletions(-) 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 1a7b1f909cf..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 @@ -24,6 +24,7 @@ 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; @@ -53,6 +54,7 @@ public class ConsumerAuthenticationFilter implements Filter { 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) { @@ -108,7 +110,7 @@ public void destroy() { } private ImmutablePair getOrCreateRateLimiterPair(String key, Integer limitCount) { - try{ + try { return LIMITER.get(key, () -> ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount))); } catch (ExecutionException e) { 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 f9b8ed5882c..fc44e76a69d 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 @@ -198,7 +198,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()); @@ -208,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); @@ -223,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 isAllowCreateApplication(List consumerIdList) { Role createAppRole = getCreateAppRole(); if (createAppRole == null) { @@ -252,6 +258,17 @@ private List isAllowCreateApplication(List consumerIdList) { return list; } + private List getRateLimit(List consumerIdList) { + List list = new ArrayList<>(consumerIdList.size()); + for (Long consumerId : consumerIdList) { + ConsumerToken consumerToken = consumerTokenRepository.findByConsumerId(consumerId); + Integer rateLimit = consumerToken.getRateLimit(); + list.add(rateLimit); + } + + return list; + } + private Role getCreateAppRole() { return rolePermissionService.findRoleByRoleName(CREATE_APPLICATION_ROLE_NAME); } @@ -405,6 +422,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()); @@ -412,7 +430,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); } 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/resources/static/open/manage.html b/apollo-portal/src/main/resources/static/open/manage.html index e1545dfa6b1..aba7a3da8d7 100644 --- a/apollo-portal/src/main/resources/static/open/manage.html +++ b/apollo-portal/src/main/resources/static/open/manage.html @@ -62,7 +62,8 @@
{{'Open.Manage.CreateThirdApp' | translate }} {{'Common.AppId' | translate }} {{'Common.AppName' | translate }} {{'Open.Manage.Consumer.AllowCreateApplication' | translate }} - {{'Common.Department' | translate }} + {{'Open.Manage.Consumer.RateLimitValue' | translate }} + {{'Common.Department' | translate }} {{'Common.AppOwner' | translate }}/{{'Common.Email' | translate }} {{'Common.Operation' | translate}} @@ -78,8 +79,9 @@
{{'Open.Manage.CreateThirdApp' | translate }} {{'Open.Manage.Consumer.AllowCreateApplication.No' | translate}}
+ {{ consumer.rateLimit }} - {{ consumer.orgName + '(' + consumer.orgId + ')' }} + {{ consumer.orgName + '(' + consumer.orgId + ')' }} {{ consumer.ownerName }}/{{ consumer.ownerEmail }}