Browse Source

Add apollo-client Authentication function

nisiyong 5 years ago
parent
commit
32aac0bd06
46 changed files with 1834 additions and 198 deletions
  1. 66 0
      apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java
  2. 21 2
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java
  3. 55 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java
  4. 20 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepository.java
  5. 86 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AccessKeyService.java
  6. 71 0
      apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepositoryTest.java
  7. 48 0
      apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/AccessKeyServiceTest.java
  8. 6 0
      apollo-biz/src/test/resources/sql/accesskey-test.sql
  9. 1 0
      apollo-biz/src/test/resources/sql/clean.sql
  10. 24 19
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java
  11. 20 14
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java
  12. 1 1
      apollo-client/src/main/java/com/ctrip/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java
  13. 9 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java
  14. 11 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpRequest.java
  15. 8 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java
  16. 72 14
      apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java
  17. 64 20
      apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java
  18. 45 0
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java
  19. 15 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java
  20. 114 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java
  21. 207 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java
  22. 58 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java
  23. 111 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java
  24. 118 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCacheTest.java
  25. 97 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtilTest.java
  26. 31 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/HmacSha1Utils.java
  27. 55 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/Signature.java
  28. 55 5
      apollo-core/src/main/java/com/ctrip/framework/foundation/internals/provider/DefaultApplicationProvider.java
  29. 5 0
      apollo-core/src/main/java/com/ctrip/framework/foundation/internals/provider/NullProvider.java
  30. 5 0
      apollo-core/src/main/java/com/ctrip/framework/foundation/spi/provider/ApplicationProvider.java
  31. 22 0
      apollo-core/src/test/java/com/ctrip/framework/apollo/core/signature/HmacSha1UtilsTest.java
  32. 36 0
      apollo-core/src/test/java/com/ctrip/framework/apollo/core/signature/SignatureTest.java
  33. 4 3
      apollo-core/src/test/java/com/ctrip/framework/foundation/internals/provider/DefaultApplicationProviderTest.java
  34. 31 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java
  35. 2 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/TracerEventType.java
  36. 77 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java
  37. 42 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java
  38. 43 45
      apollo-portal/src/main/resources/static/app/access_key.html
  39. 2 3
      apollo-portal/src/main/resources/static/config.html
  40. 20 16
      apollo-portal/src/main/resources/static/i18n/en.json
  41. 20 16
      apollo-portal/src/main/resources/static/i18n/zh-CN.json
  42. BIN
      apollo-portal/src/main/resources/static/img/secret.png
  43. 30 30
      apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js
  44. 1 7
      apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js
  45. 0 3
      apollo-portal/src/main/resources/static/scripts/services/PermissionService.js
  46. 5 0
      apollo-portal/src/main/resources/static/styles/common-style.css

+ 66 - 0
apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java

@@ -0,0 +1,66 @@
+package com.ctrip.framework.apollo.adminservice.controller;
+
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import com.ctrip.framework.apollo.biz.service.AccessKeyService;
+import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
+import com.ctrip.framework.apollo.common.utils.BeanUtils;
+import java.util.List;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author nisiyong
+ */
+@RestController
+public class AccessKeyController {
+
+  private final AccessKeyService accessKeyService;
+
+  public AccessKeyController(
+      AccessKeyService accessKeyService) {
+    this.accessKeyService = accessKeyService;
+  }
+
+  @PostMapping(value = "/apps/{appId}/accesskeys")
+  public AccessKeyDTO create(@PathVariable String appId, @RequestBody AccessKeyDTO dto) {
+    AccessKey entity = BeanUtils.transform(AccessKey.class, dto);
+    entity = accessKeyService.create(appId, entity);
+    return BeanUtils.transform(AccessKeyDTO.class, entity);
+  }
+
+  @GetMapping(value = "/apps/{appId}/accesskeys")
+  public List<AccessKeyDTO> findByAppId(@PathVariable String appId) {
+    List<AccessKey> accessKeyList = accessKeyService.findByAppId(appId);
+    return BeanUtils.batchTransform(AccessKeyDTO.class, accessKeyList);
+  }
+
+  @DeleteMapping(value = "/apps/{appId}/accesskeys/{id}")
+  public void delete(@PathVariable String appId, @PathVariable long id, String operator) {
+    accessKeyService.delete(appId, id, operator);
+  }
+
+  @PutMapping(value = "/apps/{appId}/accesskeys/{id}/enable")
+  public void enable(@PathVariable String appId, @PathVariable long id, String operator) {
+    AccessKey entity = new AccessKey();
+    entity.setId(id);
+    entity.setEnabled(true);
+    entity.setDataChangeLastModifiedBy(operator);
+
+    accessKeyService.update(appId, entity);
+  }
+
+  @PutMapping(value = "/apps/{appId}/accesskeys/{id}/disable")
+  public void disable(@PathVariable String appId, @PathVariable long id, String operator) {
+    AccessKey entity = new AccessKey();
+    entity.setId(id);
+    entity.setEnabled(false);
+    entity.setDataChangeLastModifiedBy(operator);
+
+    accessKeyService.update(appId, entity);
+  }
+}

+ 21 - 2
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/config/BizConfig.java

@@ -7,13 +7,12 @@ import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
-import org.springframework.stereotype.Component;
-
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import org.springframework.stereotype.Component;
 
 @Component
 public class BizConfig extends RefreshableConfig {
@@ -23,6 +22,8 @@ public class BizConfig extends RefreshableConfig {
   private static final int DEFAULT_APPNAMESPACE_CACHE_REBUILD_INTERVAL = 60; //60s
   private static final int DEFAULT_GRAY_RELEASE_RULE_SCAN_INTERVAL = 60; //60s
   private static final int DEFAULT_APPNAMESPACE_CACHE_SCAN_INTERVAL = 1; //1s
+  private static final int DEFAULT_ACCESSKEY_CACHE_SCAN_INTERVAL = 1; //1s
+  private static final int DEFAULT_ACCESSKEY_CACHE_REBUILD_INTERVAL = 60; //60s
   private static final int DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL = 1; //1s
   private static final int DEFAULT_RELEASE_MESSAGE_SCAN_INTERVAL_IN_MS = 1000; //1000ms
   private static final int DEFAULT_RELEASE_MESSAGE_NOTIFICATION_BATCH = 100;
@@ -119,6 +120,24 @@ public class BizConfig extends RefreshableConfig {
     return TimeUnit.SECONDS;
   }
 
+  public int accessKeyCacheScanInterval() {
+    int interval = getIntProperty("apollo.access-key-cache-scan.interval", DEFAULT_ACCESSKEY_CACHE_SCAN_INTERVAL);
+    return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_ACCESSKEY_CACHE_SCAN_INTERVAL);
+  }
+
+  public TimeUnit accessKeyCacheScanIntervalTimeUnit() {
+    return TimeUnit.SECONDS;
+  }
+
+  public int accessKeyCacheRebuildInterval() {
+    int interval = getIntProperty("apollo.access-key-cache-rebuild.interval", DEFAULT_ACCESSKEY_CACHE_REBUILD_INTERVAL);
+    return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_ACCESSKEY_CACHE_REBUILD_INTERVAL);
+  }
+
+  public TimeUnit accessKeyCacheRebuildIntervalTimeUnit() {
+    return TimeUnit.SECONDS;
+  }
+
   public int releaseMessageCacheScanInterval() {
     int interval = getIntProperty("apollo.release-message-cache-scan.interval", DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL);
     return checkInt(interval, 1, Integer.MAX_VALUE, DEFAULT_RELEASE_MESSAGE_CACHE_SCAN_INTERVAL);

+ 55 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java

@@ -0,0 +1,55 @@
+package com.ctrip.framework.apollo.biz.entity;
+
+import com.ctrip.framework.apollo.common.entity.BaseEntity;
+import org.hibernate.annotations.SQLDelete;
+import org.hibernate.annotations.Where;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "AccessKey")
+@SQLDelete(sql = "Update AccessKey set isDeleted = 1 where id = ?")
+@Where(clause = "isDeleted = 0")
+public class AccessKey extends BaseEntity {
+
+  @Column(name = "appId", nullable = false)
+  private String appId;
+
+  @Column(name = "Secret", nullable = false)
+  private String secret;
+
+  @Column(name = "isEnabled", columnDefinition = "Bit default '0'")
+  private boolean enabled;
+
+  public String getAppId() {
+    return appId;
+  }
+
+  public void setAppId(String appId) {
+    this.appId = appId;
+  }
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public void setSecret(String secret) {
+    this.secret = secret;
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  @Override
+  public String toString() {
+    return toStringHelper().add("appId", appId).add("secret", secret)
+        .add("enabled", enabled).toString();
+  }
+}

+ 20 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepository.java

@@ -0,0 +1,20 @@
+package com.ctrip.framework.apollo.biz.repository;
+
+
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import java.util.Date;
+import java.util.List;
+import org.springframework.data.repository.PagingAndSortingRepository;
+
+public interface AccessKeyRepository extends PagingAndSortingRepository<AccessKey, Long> {
+
+  long countByAppId(String appId);
+
+  AccessKey findOneByAppIdAndId(String appId, long id);
+
+  List<AccessKey> findByAppId(String appId);
+
+  List<AccessKey> findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(Date date);
+
+  List<AccessKey> findByDataChangeLastModifiedTime(Date date);
+}

+ 86 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AccessKeyService.java

@@ -0,0 +1,86 @@
+package com.ctrip.framework.apollo.biz.service;
+
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import com.ctrip.framework.apollo.biz.entity.Audit;
+import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository;
+import com.ctrip.framework.apollo.common.exception.BadRequestException;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author nisiyong
+ */
+@Service
+public class AccessKeyService {
+
+  private static final int ACCESSKEY_COUNT_LIMIT = 5;
+
+  private final AccessKeyRepository accessKeyRepository;
+  private final AuditService auditService;
+
+  public AccessKeyService(
+      AccessKeyRepository accessKeyRepository,
+      AuditService auditService) {
+    this.accessKeyRepository = accessKeyRepository;
+    this.auditService = auditService;
+  }
+
+  public List<AccessKey> findByAppId(String appId) {
+    return accessKeyRepository.findByAppId(appId);
+  }
+
+  @Transactional
+  public AccessKey create(String appId, AccessKey entity) {
+    long count = accessKeyRepository.countByAppId(appId);
+    if (count >= ACCESSKEY_COUNT_LIMIT) {
+      throw new BadRequestException("AccessKeys count limit exceeded");
+    }
+
+    entity.setId(0L);
+    entity.setAppId(appId);
+    entity.setDataChangeLastModifiedBy(entity.getDataChangeCreatedBy());
+    AccessKey accessKey = accessKeyRepository.save(entity);
+
+    auditService.audit(AccessKey.class.getSimpleName(), accessKey.getId(), Audit.OP.INSERT,
+        accessKey.getDataChangeCreatedBy());
+
+    return accessKey;
+  }
+
+  @Transactional
+  public AccessKey update(String appId, AccessKey entity) {
+    long id = entity.getId();
+    String operator = entity.getDataChangeLastModifiedBy();
+
+    AccessKey accessKey = accessKeyRepository.findOneByAppIdAndId(appId, id);
+    if (accessKey == null) {
+      throw new BadRequestException("AccessKey not exist");
+    }
+
+    accessKey.setEnabled(entity.isEnabled());
+    accessKey.setDataChangeLastModifiedBy(operator);
+    accessKeyRepository.save(accessKey);
+
+    auditService.audit(AccessKey.class.getSimpleName(), id, Audit.OP.UPDATE, operator);
+    return accessKey;
+  }
+
+  @Transactional
+  public void delete(String appId, long id, String operator) {
+    AccessKey accessKey = accessKeyRepository.findOneByAppIdAndId(appId, id);
+    if (accessKey == null) {
+      throw new BadRequestException("AccessKey not exist");
+    }
+
+    if (accessKey.isEnabled()) {
+      throw new BadRequestException("AccessKey should disable first");
+    }
+
+    accessKey.setDeleted(Boolean.TRUE);
+    accessKey.setDataChangeLastModifiedBy(operator);
+    accessKeyRepository.save(accessKey);
+
+    auditService.audit(AccessKey.class.getSimpleName(), id, Audit.OP.DELETE, operator);
+  }
+}

+ 71 - 0
apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/repository/AccessKeyRepositoryTest.java

@@ -0,0 +1,71 @@
+package com.ctrip.framework.apollo.biz.repository;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.ctrip.framework.apollo.biz.AbstractIntegrationTest;
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.List;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
+
+
+public class AccessKeyRepositoryTest extends AbstractIntegrationTest {
+
+  @Autowired
+  private AccessKeyRepository accessKeyRepository;
+
+  @Test
+  public void testSave() {
+    String appId = "someAppId";
+    String secret = "someSecret";
+    AccessKey entity = new AccessKey();
+    entity.setAppId(appId);
+    entity.setSecret(secret);
+
+    AccessKey accessKey = accessKeyRepository.save(entity);
+
+    assertThat(accessKey).isNotNull();
+    assertThat(accessKey.getAppId()).isEqualTo(appId);
+    assertThat(accessKey.getSecret()).isEqualTo(secret);
+  }
+
+  @Test
+  @Sql(scripts = "/sql/accesskey-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
+  public void testFindByAppId() {
+    String appId = "someAppId";
+
+    List<AccessKey> accessKeyList = accessKeyRepository.findByAppId(appId);
+
+    assertThat(accessKeyList).hasSize(1);
+    assertThat(accessKeyList.get(0).getAppId()).isEqualTo(appId);
+    assertThat(accessKeyList.get(0).getSecret()).isEqualTo("someSecret");
+  }
+
+  @Test
+  @Sql(scripts = "/sql/accesskey-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/sql/clean.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD)
+  public void testFindFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTime() {
+    Instant instant = LocalDateTime.of(2019, 12, 19, 13, 44, 20)
+        .atZone(ZoneId.systemDefault())
+        .toInstant();
+    Date date = Date.from(instant);
+
+    List<AccessKey> accessKeyList = accessKeyRepository
+        .findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(date);
+
+    assertThat(accessKeyList).hasSize(2);
+    assertThat(accessKeyList.get(0).getAppId()).isEqualTo("100004458");
+    assertThat(accessKeyList.get(0).getSecret()).isEqualTo("4003c4d7783443dc9870932bebf3b7fe");
+    assertThat(accessKeyList.get(1).getAppId()).isEqualTo("100004458");
+    assertThat(accessKeyList.get(1).getSecret()).isEqualTo("c715cbc80fc44171b43732c3119c9456");
+  }
+
+}

+ 48 - 0
apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/service/AccessKeyServiceTest.java

@@ -0,0 +1,48 @@
+package com.ctrip.framework.apollo.biz.service;
+
+import static org.junit.Assert.assertNotNull;
+
+import com.ctrip.framework.apollo.biz.AbstractIntegrationTest;
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import com.ctrip.framework.apollo.common.exception.BadRequestException;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author nisiyong
+ */
+public class AccessKeyServiceTest extends AbstractIntegrationTest {
+
+  @Autowired
+  private AccessKeyService accessKeyService;
+
+  @Test
+  public void testCreate() {
+    String appId = "someAppId";
+    String secret = "someSecret";
+    AccessKey entity = assembleAccessKey(appId, secret);
+
+    AccessKey accessKey = accessKeyService.create(appId, entity);
+
+    assertNotNull(accessKey);
+  }
+
+  @Test(expected = BadRequestException.class)
+  public void testCreateWithException() {
+    String appId = "someAppId";
+    String secret = "someSecret";
+    int maxCount = 5;
+
+    for (int i = 0; i <= maxCount; i++) {
+      AccessKey entity = assembleAccessKey(appId, secret);
+      accessKeyService.create(appId, entity);
+    }
+  }
+
+  private AccessKey assembleAccessKey(String appId, String secret) {
+    AccessKey accessKey = new AccessKey();
+    accessKey.setAppId(appId);
+    accessKey.setSecret(secret);
+    return accessKey;
+  }
+}

+ 6 - 0
apollo-biz/src/test/resources/sql/accesskey-test.sql

@@ -0,0 +1,6 @@
+INSERT INTO `AccessKey` (`Id`, `AppId`, `Secret`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`)
+VALUES
+	(1, 'someAppId', 'someSecret', 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'),
+	(2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'),
+	(3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'),
+	(4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21');

+ 1 - 0
apollo-biz/src/test/resources/sql/clean.sql

@@ -1,3 +1,4 @@
+DELETE FROM AccessKey;
 DELETE FROM App;
 DELETE FROM AppNamespace;
 DELETE FROM Cluster;

+ 24 - 19
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java

@@ -1,18 +1,5 @@
 package com.ctrip.framework.apollo.internals;
 
-import com.google.common.base.Joiner;
-import com.google.common.base.Strings;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.escape.Escaper;
-import com.google.common.net.UrlEscapers;
-import com.google.common.reflect.TypeToken;
-import com.google.common.util.concurrent.RateLimiter;
-import com.google.gson.Gson;
-
 import com.ctrip.framework.apollo.build.ApolloInjector;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
@@ -21,7 +8,9 @@ import com.ctrip.framework.apollo.core.dto.ServiceDTO;
 import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
 import com.ctrip.framework.apollo.core.schedule.ExponentialSchedulePolicy;
 import com.ctrip.framework.apollo.core.schedule.SchedulePolicy;
+import com.ctrip.framework.apollo.core.signature.Signature;
 import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
+import com.ctrip.framework.apollo.core.utils.StringUtils;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
 import com.ctrip.framework.apollo.tracer.Tracer;
 import com.ctrip.framework.apollo.tracer.spi.Transaction;
@@ -30,10 +19,18 @@ import com.ctrip.framework.apollo.util.ExceptionUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
 import com.ctrip.framework.apollo.util.http.HttpUtil;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.RateLimiter;
+import com.google.gson.Gson;
 import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Map;
@@ -43,6 +40,8 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
@@ -109,6 +108,7 @@ public class RemoteConfigLongPollService {
       final String appId = m_configUtil.getAppId();
       final String cluster = m_configUtil.getCluster();
       final String dataCenter = m_configUtil.getDataCenter();
+      final String secret = m_configUtil.getAccessKeySecret();
       final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
       m_longPollingService.submit(new Runnable() {
         @Override
@@ -121,7 +121,7 @@ public class RemoteConfigLongPollService {
               //ignore
             }
           }
-          doLongPollingRefresh(appId, cluster, dataCenter);
+          doLongPollingRefresh(appId, cluster, dataCenter, secret);
         }
       });
     } catch (Throwable ex) {
@@ -137,7 +137,7 @@ public class RemoteConfigLongPollService {
     this.m_longPollingStopped.compareAndSet(false, true);
   }
 
-  private void doLongPollingRefresh(String appId, String cluster, String dataCenter) {
+  private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {
     final Random random = new Random();
     ServiceDTO lastServiceDto = null;
     while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
@@ -161,8 +161,13 @@ public class RemoteConfigLongPollService {
                 m_notifications);
 
         logger.debug("Long polling from {}", url);
+
         HttpRequest request = new HttpRequest(url);
         request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
+        if (!StringUtils.isBlank(secret)) {
+          Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
+          request.setHeaders(headers);
+        }
 
         transaction.addData("Url", url);
 

+ 20 - 14
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java

@@ -1,19 +1,5 @@
 package com.ctrip.framework.apollo.internals;
 
-import com.ctrip.framework.apollo.enums.ConfigSourceType;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import com.ctrip.framework.apollo.Apollo;
 import com.ctrip.framework.apollo.build.ApolloInjector;
 import com.ctrip.framework.apollo.core.ConfigConsts;
@@ -22,7 +8,10 @@ import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages;
 import com.ctrip.framework.apollo.core.dto.ServiceDTO;
 import com.ctrip.framework.apollo.core.schedule.ExponentialSchedulePolicy;
 import com.ctrip.framework.apollo.core.schedule.SchedulePolicy;
+import com.ctrip.framework.apollo.core.signature.Signature;
 import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigStatusCodeException;
 import com.ctrip.framework.apollo.tracer.Tracer;
@@ -40,6 +29,17 @@ import com.google.common.escape.Escaper;
 import com.google.common.net.UrlEscapers;
 import com.google.common.util.concurrent.RateLimiter;
 import com.google.gson.Gson;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
@@ -174,6 +174,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
     String appId = m_configUtil.getAppId();
     String cluster = m_configUtil.getCluster();
     String dataCenter = m_configUtil.getDataCenter();
+    String secret = m_configUtil.getAccessKeySecret();
     Tracer.logEvent("Apollo.Client.ConfigMeta", STRING_JOINER.join(appId, cluster, m_namespace));
     int maxRetries = m_configNeedForceRefresh.get() ? 2 : 1;
     long onErrorSleepTime = 0; // 0 means no sleep
@@ -206,7 +207,12 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
                 dataCenter, m_remoteMessages.get(), m_configCache.get());
 
         logger.debug("Loading config from {}", url);
+
         HttpRequest request = new HttpRequest(url);
+        if (!StringUtils.isBlank(secret)) {
+          Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
+          request.setHeaders(headers);
+        }
 
         Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "queryConfig");
         transaction.addData("Url", url);

+ 1 - 1
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/boot/ApolloApplicationContextInitializer.java

@@ -61,7 +61,7 @@ public class ApolloApplicationContextInitializer implements
   private static final Logger logger = LoggerFactory.getLogger(ApolloApplicationContextInitializer.class);
   private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();
   private static final String[] APOLLO_SYSTEM_PROPERTIES = {"app.id", ConfigConsts.APOLLO_CLUSTER_KEY,
-      "apollo.cacheDir", ConfigConsts.APOLLO_META_KEY};
+      "apollo.cacheDir", "apollo.accesskey.secret", ConfigConsts.APOLLO_META_KEY};
 
   private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector
       .getInstance(ConfigPropertySourceFactory.class);

+ 9 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java

@@ -66,6 +66,15 @@ public class ConfigUtil {
     return appId;
   }
 
+  /**
+   * Get the access key secret for the current application.
+   *
+   * @return the current access key secret, null if there is no such secret.
+   */
+  public String getAccessKeySecret() {
+    return Foundation.app().getAccessKeySecret();
+  }
+
   /**
    * Get the data center info for the current application.
    *

+ 11 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpRequest.java

@@ -1,10 +1,13 @@
 package com.ctrip.framework.apollo.util.http;
 
+import java.util.Map;
+
 /**
  * @author Jason Song(song_s@ctrip.com)
  */
 public class HttpRequest {
   private String m_url;
+  private Map<String, String> headers;
   private int m_connectTimeout;
   private int m_readTimeout;
 
@@ -22,6 +25,14 @@ public class HttpRequest {
     return m_url;
   }
 
+  public Map<String, String> getHeaders() {
+    return headers;
+  }
+
+  public void setHeaders(Map<String, String> headers) {
+    this.headers = headers;
+  }
+
   public int getConnectTimeout() {
     return m_connectTimeout;
   }

+ 8 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java

@@ -14,6 +14,7 @@ import java.lang.reflect.Type;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.Map;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
@@ -78,6 +79,13 @@ public class HttpUtil {
 
       conn.setRequestMethod("GET");
 
+      Map<String, String> headers = httpRequest.getHeaders();
+      if (headers != null && headers.size() > 0) {
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+          conn.setRequestProperty(entry.getKey(), entry.getValue());
+        }
+      }
+
       int connectTimeout = httpRequest.getConnectTimeout();
       if (connectTimeout < 0) {
         connectTimeout = m_configUtil.getConnectTimeout();

+ 72 - 14
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java

@@ -1,6 +1,7 @@
 package com.ctrip.framework.apollo.internals;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
@@ -11,14 +12,24 @@ import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.ctrip.framework.apollo.build.MockInjector;
+import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
+import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages;
+import com.ctrip.framework.apollo.core.dto.ServiceDTO;
+import com.ctrip.framework.apollo.core.signature.Signature;
+import com.ctrip.framework.apollo.util.ConfigUtil;
+import com.ctrip.framework.apollo.util.http.HttpRequest;
+import com.ctrip.framework.apollo.util.http.HttpResponse;
+import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.SettableFuture;
 import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
-
 import javax.servlet.http.HttpServletResponse;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,18 +40,6 @@ import org.mockito.runners.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
 import org.springframework.test.util.ReflectionTestUtils;
 
-import com.ctrip.framework.apollo.build.MockInjector;
-import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
-import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages;
-import com.ctrip.framework.apollo.core.dto.ServiceDTO;
-import com.ctrip.framework.apollo.util.ConfigUtil;
-import com.ctrip.framework.apollo.util.http.HttpRequest;
-import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.SettableFuture;
-
 /**
  * @author Jason Song(song_s@ctrip.com)
  */
@@ -58,6 +57,7 @@ public class RemoteConfigLongPollServiceTest {
   private static String someServerUrl;
   private static String someAppId;
   private static String someCluster;
+  private static String someSecret;
 
   @Before
   public void setUp() throws Exception {
@@ -178,6 +178,59 @@ public class RemoteConfigLongPollServiceTest {
     assertEquals(anotherNotificationId, captured.get(anotherKey).longValue());
   }
 
+  @Test
+  public void testSubmitLongPollNamespaceWithAccessKeySecret() throws Exception {
+    someSecret = "someSecret";
+    RemoteConfigRepository someRepository = mock(RemoteConfigRepository.class);
+    final String someNamespace = "someNamespace";
+    ApolloNotificationMessages notificationMessages = new ApolloNotificationMessages();
+    String someKey = "someKey";
+    long someNotificationId = 1;
+    notificationMessages.put(someKey, someNotificationId);
+
+    ApolloConfigNotification someNotification = mock(ApolloConfigNotification.class);
+    when(someNotification.getNamespaceName()).thenReturn(someNamespace);
+    when(someNotification.getMessages()).thenReturn(notificationMessages);
+
+    when(pollResponse.getStatusCode()).thenReturn(HttpServletResponse.SC_OK);
+    when(pollResponse.getBody()).thenReturn(Lists.newArrayList(someNotification));
+
+    doAnswer(new Answer<HttpResponse<List<ApolloConfigNotification>>>() {
+      @Override
+      public HttpResponse<List<ApolloConfigNotification>> answer(InvocationOnMock invocation)
+          throws Throwable {
+        try {
+          TimeUnit.MILLISECONDS.sleep(50);
+        } catch (InterruptedException e) {
+        }
+
+        HttpRequest request = invocation.getArgumentAt(0, HttpRequest.class);
+
+        Map<String, String> headers = request.getHeaders();
+        assertNotNull(headers);
+        assertTrue(headers.containsKey(Signature.HTTP_HEADER_TIMESTAMP));
+        assertTrue(headers.containsKey(Signature.HTTP_HEADER_AUTHORIZATION));
+
+        return pollResponse;
+      }
+    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+
+    final SettableFuture<Boolean> onNotified = SettableFuture.create();
+    doAnswer(new Answer<Void>() {
+      @Override
+      public Void answer(InvocationOnMock invocation) throws Throwable {
+        onNotified.set(true);
+        return null;
+      }
+    }).when(someRepository).onLongPollNotified(any(ServiceDTO.class), any(ApolloNotificationMessages.class));
+
+    remoteConfigLongPollService.submit(someNamespace, someRepository);
+    onNotified.get(5000, TimeUnit.MILLISECONDS);
+    remoteConfigLongPollService.stopLongPollingRefresh();
+
+    verify(someRepository, times(1)).onLongPollNotified(any(ServiceDTO.class), any(ApolloNotificationMessages.class));
+  }
+
   @Test
   public void testSubmitLongPollMultipleNamespaces() throws Exception {
     RemoteConfigRepository someRepository = mock(RemoteConfigRepository.class);
@@ -476,6 +529,11 @@ public class RemoteConfigLongPollServiceTest {
       return someCluster;
     }
 
+    @Override
+    public String getAccessKeySecret() {
+      return someSecret;
+    }
+
     @Override
     public String getDataCenter() {
       return null;

+ 64 - 20
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java

@@ -1,6 +1,7 @@
 package com.ctrip.framework.apollo.internals;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.any;
@@ -12,29 +13,13 @@ import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import com.ctrip.framework.apollo.enums.ConfigSourceType;
-import java.lang.reflect.Type;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
-
-import javax.servlet.http.HttpServletResponse;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.mockito.stubbing.Answer;
-
 import com.ctrip.framework.apollo.build.MockInjector;
 import com.ctrip.framework.apollo.core.dto.ApolloConfig;
 import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
 import com.ctrip.framework.apollo.core.dto.ApolloNotificationMessages;
 import com.ctrip.framework.apollo.core.dto.ServiceDTO;
+import com.ctrip.framework.apollo.core.signature.Signature;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
 import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
@@ -46,6 +31,20 @@ import com.google.common.collect.Maps;
 import com.google.common.net.UrlEscapers;
 import com.google.common.util.concurrent.SettableFuture;
 import com.google.gson.Gson;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
 
 /**
  * Created by Jason on 4/9/16.
@@ -64,6 +63,10 @@ public class RemoteConfigRepositoryTest {
   private static HttpResponse<List<ApolloConfigNotification>> pollResponse;
   private RemoteConfigLongPollService remoteConfigLongPollService;
 
+  private static String someAppId;
+  private static String someCluster;
+  private static String someSecret;
+
   @Before
   public void setUp() throws Exception {
     someNamespace = "someName";
@@ -88,6 +91,9 @@ public class RemoteConfigRepositoryTest {
     remoteConfigLongPollService = new RemoteConfigLongPollService();
 
     MockInjector.setInstance(RemoteConfigLongPollService.class, remoteConfigLongPollService);
+
+    someAppId = "someAppId";
+    someCluster = "someCluster";
   }
 
   @Test
@@ -110,6 +116,39 @@ public class RemoteConfigRepositoryTest {
     remoteConfigLongPollService.stopLongPollingRefresh();
   }
 
+  @Test
+  public void testLoadConfigWithAccessKeySecret() throws Exception {
+    someSecret = "someSecret";
+    String someKey = "someKey";
+    String someValue = "someValue";
+    Map<String, String> configurations = Maps.newHashMap();
+    configurations.put(someKey, someValue);
+    ApolloConfig someApolloConfig = assembleApolloConfig(configurations);
+
+    when(someResponse.getStatusCode()).thenReturn(200);
+    when(someResponse.getBody()).thenReturn(someApolloConfig);
+    doAnswer(new Answer<HttpResponse<ApolloConfig>>() {
+      @Override
+      public HttpResponse<ApolloConfig> answer(InvocationOnMock invocation) throws Throwable {
+        HttpRequest request = invocation.getArgumentAt(0, HttpRequest.class);
+        Map<String, String> headers = request.getHeaders();
+        assertNotNull(headers);
+        assertTrue(headers.containsKey(Signature.HTTP_HEADER_TIMESTAMP));
+        assertTrue(headers.containsKey(Signature.HTTP_HEADER_AUTHORIZATION));
+
+        return someResponse;
+      }
+    }).when(httpUtil).doGet(any(HttpRequest.class), any(Class.class));
+
+    RemoteConfigRepository remoteConfigRepository = new RemoteConfigRepository(someNamespace);
+
+    Properties config = remoteConfigRepository.getConfig();
+
+    assertEquals(configurations, config);
+    assertEquals(ConfigSourceType.REMOTE, remoteConfigRepository.getSourceType());
+    remoteConfigLongPollService.stopLongPollingRefresh();
+  }
+
   @Test(expected = ApolloConfigException.class)
   public void testGetRemoteConfigWithServerError() throws Exception {
 
@@ -254,12 +293,17 @@ public class RemoteConfigRepositoryTest {
   public static class MockConfigUtil extends ConfigUtil {
     @Override
     public String getAppId() {
-      return "someApp";
+      return someAppId;
     }
 
     @Override
     public String getCluster() {
-      return "someCluster";
+      return someCluster;
+    }
+
+    @Override
+    public String getAccessKeySecret() {
+      return someSecret;
     }
 
     @Override

+ 45 - 0
apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java

@@ -0,0 +1,45 @@
+package com.ctrip.framework.apollo.common.dto;
+
+public class AccessKeyDTO extends BaseDTO {
+
+  private Long id;
+
+  private String secret;
+
+  private String appId;
+
+  private Boolean enabled;
+
+  public Long getId() {
+    return id;
+  }
+
+  public void setId(Long id) {
+    this.id = id;
+  }
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public void setSecret(String secret) {
+    this.secret = secret;
+  }
+
+  public String getAppId() {
+    return appId;
+  }
+
+  public void setAppId(String appId) {
+    this.appId = appId;
+  }
+
+  public Boolean getEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(Boolean enabled) {
+    this.enabled = enabled;
+  }
+
+}

+ 15 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java

@@ -6,10 +6,13 @@ import com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner;
 import com.ctrip.framework.apollo.configservice.controller.ConfigFileController;
 import com.ctrip.framework.apollo.configservice.controller.NotificationController;
 import com.ctrip.framework.apollo.configservice.controller.NotificationControllerV2;
+import com.ctrip.framework.apollo.configservice.filter.ClientAuthenticationFilter;
 import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache;
 import com.ctrip.framework.apollo.configservice.service.config.ConfigService;
 import com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCache;
 import com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService;
+import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@@ -44,6 +47,18 @@ public class ConfigServiceAutoConfiguration {
     return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
   }
 
+  @Bean
+  public FilterRegistrationBean clientAuthenticationFilter(AccessKeyUtil accessKeyUtil) {
+    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
+
+    filterRegistrationBean.setFilter(new ClientAuthenticationFilter(accessKeyUtil));
+    filterRegistrationBean.addUrlPatterns("/configs/*");
+    filterRegistrationBean.addUrlPatterns("/configfiles/*");
+    filterRegistrationBean.addUrlPatterns("/notifications/v2/*");
+
+    return filterRegistrationBean;
+  }
+
   @Configuration
   static class MessageScannerConfiguration {
     private final NotificationController notificationController;

+ 114 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java

@@ -0,0 +1,114 @@
+package com.ctrip.framework.apollo.configservice.filter;
+
+import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
+import com.ctrip.framework.apollo.core.signature.Signature;
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author nisiyong
+ */
+public class ClientAuthenticationFilter implements Filter {
+
+  private static final Logger logger = LoggerFactory.getLogger(ClientAuthenticationFilter.class);
+
+  private static final Long TIMESTAMP_INTERVAL = 60 * 1000L;
+
+  private final AccessKeyUtil accessKeyUtil;
+
+  public ClientAuthenticationFilter(AccessKeyUtil accessKeyUtil) {
+    this.accessKeyUtil = accessKeyUtil;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    //nothing
+  }
+
+  @Override
+  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
+      throws IOException, ServletException {
+    HttpServletRequest request = (HttpServletRequest) req;
+    HttpServletResponse response = (HttpServletResponse) resp;
+
+    String appId = accessKeyUtil.extractAppIdFromRequest(request);
+    if (StringUtils.isBlank(appId)) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId");
+      return;
+    }
+
+    List<String> availableSecrets = accessKeyUtil.findAvailableSecret(appId);
+    if (!CollectionUtils.isEmpty(availableSecrets)) {
+      String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
+      String authorization = request.getHeader(Signature.HTTP_HEADER_AUTHORIZATION);
+
+      // check timestamp, valid within 1 minute
+      if (!checkTimestamp(timestamp)) {
+        logger.warn("Invalid timestamp. appId={},timestamp={}", appId, timestamp);
+        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
+        return;
+      }
+
+      // check signature
+      String path = request.getServletPath();
+      String query = request.getQueryString();
+      if (!checkAuthorization(authorization, availableSecrets, timestamp, path, query)) {
+        logger.warn("Invalid authorization. appId={},authorization={}", appId, authorization);
+        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+        return;
+      }
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  @Override
+  public void destroy() {
+    //nothing
+  }
+
+  private boolean checkTimestamp(String timestamp) {
+    long requestTimeMillis = 0L;
+    try {
+      requestTimeMillis = Long.parseLong(timestamp);
+    } catch (NumberFormatException e) {
+      // nothing to do
+    }
+
+    long x = System.currentTimeMillis() - requestTimeMillis;
+    return x <= TIMESTAMP_INTERVAL;
+  }
+
+  private boolean checkAuthorization(String authorization, List<String> availableSecrets,
+      String timestamp, String path, String query) {
+
+    String signature = null;
+    if (authorization != null) {
+      String[] split = authorization.split(":");
+      if (split.length > 1) {
+        signature = split[1];
+      }
+    }
+
+    for (String secret : availableSecrets) {
+      String availableSignature = accessKeyUtil.buildSignature(path, query, timestamp, secret);
+      if (Objects.equals(signature, availableSignature)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}

+ 207 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java

@@ -0,0 +1,207 @@
+package com.ctrip.framework.apollo.configservice.service;
+
+import com.ctrip.framework.apollo.biz.config.BizConfig;
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository;
+import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
+import com.ctrip.framework.apollo.tracer.Tracer;
+import com.ctrip.framework.apollo.tracer.spi.Transaction;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.MultimapBuilder.ListMultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author nisiyong
+ */
+@Service
+public class AccessKeyServiceWithCache implements InitializingBean {
+
+  private static Logger logger = LoggerFactory.getLogger(AccessKeyServiceWithCache.class);
+
+  private final AccessKeyRepository accessKeyRepository;
+  private final BizConfig bizConfig;
+
+  private int scanInterval;
+  private TimeUnit scanIntervalTimeUnit;
+  private int rebuildInterval;
+  private TimeUnit rebuildIntervalTimeUnit;
+  private ScheduledExecutorService scheduledExecutorService;
+  private Date lastTimeScanned;
+
+  private ListMultimap<String, AccessKey> accessKeyCache;
+  private ConcurrentMap<Long, AccessKey> accessKeyIdCache;
+
+  @Autowired
+  public AccessKeyServiceWithCache(AccessKeyRepository accessKeyRepository, BizConfig bizConfig) {
+    this.accessKeyRepository = accessKeyRepository;
+    this.bizConfig = bizConfig;
+
+    initialize();
+  }
+
+  private void initialize() {
+    scheduledExecutorService = new ScheduledThreadPoolExecutor(1,
+        ApolloThreadFactory.create("AccessKeyServiceWithCache", true));
+    lastTimeScanned = new Date(0L);
+
+    ListMultimap<String, AccessKey> multimap = ListMultimapBuilder.hashKeys(128)
+        .arrayListValues().build();
+    accessKeyCache = Multimaps.synchronizedListMultimap(multimap);
+    accessKeyIdCache = Maps.newConcurrentMap();
+  }
+
+  public List<String> getAvailableSecrets(String appId) {
+    List<AccessKey> accessKeys = accessKeyCache.get(appId);
+    if (CollectionUtils.isEmpty(accessKeys)) {
+      return Collections.emptyList();
+    }
+
+    return accessKeys.stream()
+        .filter(AccessKey::isEnabled)
+        .map(AccessKey::getSecret)
+        .collect(Collectors.toList());
+  }
+
+  @Override
+  public void afterPropertiesSet() throws Exception {
+    populateDataBaseInterval();
+    scanNewAndUpdatedAccessKeys(); //block the startup process until load finished
+
+    scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAndUpdatedAccessKeys,
+        scanInterval, scanInterval, scanIntervalTimeUnit);
+
+    scheduledExecutorService.scheduleAtFixedRate(this::rebuildAccessKeyCache,
+        rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit);
+  }
+
+  private void scanNewAndUpdatedAccessKeys() {
+    Transaction transaction = Tracer.newTransaction("Apollo.AccessKeyServiceWithCache",
+        "scanNewAndUpdatedAccessKeys");
+    try {
+      loadNewAndUpdatedAccessKeys();
+      transaction.setStatus(Transaction.SUCCESS);
+    } catch (Throwable ex) {
+      transaction.setStatus(ex);
+      logger.error("Load new/updated app access keys failed", ex);
+    } finally {
+      transaction.complete();
+    }
+  }
+
+  private void rebuildAccessKeyCache() {
+    Transaction transaction = Tracer.newTransaction("Apollo.AccessKeyServiceWithCache",
+        "rebuildCache");
+    try {
+      deleteAccessKeyCache();
+      transaction.setStatus(Transaction.SUCCESS);
+    } catch (Throwable ex) {
+      transaction.setStatus(ex);
+      logger.error("Rebuild cache failed", ex);
+    } finally {
+      transaction.complete();
+    }
+  }
+
+  private void loadNewAndUpdatedAccessKeys() {
+    boolean hasMore = true;
+    while (hasMore && !Thread.currentThread().isInterrupted()) {
+      //current batch is 500
+      List<AccessKey> accessKeys = accessKeyRepository
+          .findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(lastTimeScanned);
+      if (CollectionUtils.isEmpty(accessKeys)) {
+        break;
+      }
+
+      int scanned = accessKeys.size();
+      mergeAccessKeys(accessKeys);
+      logger.info("Loaded {} new/updated Accesskey from startTime {}", scanned, lastTimeScanned);
+
+      hasMore = scanned == 500;
+      lastTimeScanned = accessKeys.get(scanned - 1).getDataChangeLastModifiedTime();
+
+      // In order to avoid missing some records at the last time, we need to scan records at this time individually
+      if (hasMore) {
+        List<AccessKey> lastModifiedTimeAccessKeys = accessKeyRepository.findByDataChangeLastModifiedTime(lastTimeScanned);
+        mergeAccessKeys(lastModifiedTimeAccessKeys);
+        logger.info("Loaded {} new/updated Accesskey at lastModifiedTime {}", scanned, lastTimeScanned);
+      }
+    }
+  }
+
+  private void mergeAccessKeys(List<AccessKey> accessKeys) {
+    for (AccessKey accessKey : accessKeys) {
+      AccessKey thatInCache = accessKeyIdCache.get(accessKey.getId());
+
+      accessKeyIdCache.put(accessKey.getId(), accessKey);
+      accessKeyCache.put(accessKey.getAppId(), accessKey);
+
+      if (thatInCache != null && accessKey.getDataChangeLastModifiedTime()
+          .after(thatInCache.getDataChangeLastModifiedTime())) {
+        accessKeyCache.remove(accessKey.getAppId(), thatInCache);
+        logger.info("Found Accesskey changes, old: {}, new: {}", thatInCache, accessKey);
+      }
+    }
+  }
+
+  private void deleteAccessKeyCache() {
+    List<Long> ids = Lists.newArrayList(accessKeyIdCache.keySet());
+    if (CollectionUtils.isEmpty(ids)) {
+      return;
+    }
+
+    List<List<Long>> partitionIds = Lists.partition(ids, 500);
+    for (List<Long> toRebuildIds : partitionIds) {
+      Iterable<AccessKey> accessKeys = accessKeyRepository.findAllById(toRebuildIds);
+
+      Set<Long> foundIds = Sets.newHashSet();
+      for (AccessKey accessKey : accessKeys) {
+        foundIds.add(accessKey.getId());
+      }
+
+      //handle deleted
+      SetView<Long> deletedIds = Sets.difference(Sets.newHashSet(toRebuildIds), foundIds);
+      handleDeletedAccessKeys(deletedIds);
+    }
+  }
+
+  private void handleDeletedAccessKeys(Set<Long> deletedIds) {
+    if (CollectionUtils.isEmpty(deletedIds)) {
+      return;
+    }
+    for (Long deletedId : deletedIds) {
+      AccessKey deleted = accessKeyIdCache.remove(deletedId);
+      if (deleted == null) {
+        continue;
+      }
+
+      accessKeyCache.remove(deleted.getAppId(), deleted);
+      logger.info("Found AccessKey deleted, {}", deleted);
+    }
+  }
+
+  private void populateDataBaseInterval() {
+    scanInterval = bizConfig.accessKeyCacheScanInterval();
+    scanIntervalTimeUnit = bizConfig.accessKeyCacheScanIntervalTimeUnit();
+    rebuildInterval = bizConfig.accessKeyCacheRebuildInterval();
+    rebuildIntervalTimeUnit = bizConfig.accessKeyCacheRebuildIntervalTimeUnit();
+  }
+}

+ 58 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java

@@ -0,0 +1,58 @@
+package com.ctrip.framework.apollo.configservice.util;
+
+import com.ctrip.framework.apollo.configservice.service.AccessKeyServiceWithCache;
+import com.ctrip.framework.apollo.core.signature.Signature;
+import com.google.common.base.Strings;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author nisiyong
+ */
+@Component
+public class AccessKeyUtil {
+
+  private static final String URL_SEPARATOR = "/";
+  private static final String URL_CONFIGS_PREFIX = "/configs/";
+  private static final String URL_CONFIGFILES_JSON_PREFIX = "/configfiles/json/";
+  private static final String URL_CONFIGFILES_PREFIX = "/configfiles/";
+  private static final String URL_NOTIFICATIONS_PREFIX = "/notifications/v2";
+
+  private final AccessKeyServiceWithCache accessKeyServiceWithCache;
+
+  public AccessKeyUtil(AccessKeyServiceWithCache accessKeyServiceWithCache) {
+    this.accessKeyServiceWithCache = accessKeyServiceWithCache;
+  }
+
+  public List<String> findAvailableSecret(String appId) {
+    return accessKeyServiceWithCache.getAvailableSecrets(appId);
+  }
+
+  public String extractAppIdFromRequest(HttpServletRequest request) {
+    String appId = null;
+    String servletPath = request.getServletPath();
+
+    if (StringUtils.startsWith(servletPath, URL_CONFIGS_PREFIX)) {
+      appId = StringUtils.substringBetween(servletPath, URL_CONFIGS_PREFIX, URL_SEPARATOR);
+    } else if (StringUtils.startsWith(servletPath, URL_CONFIGFILES_JSON_PREFIX)) {
+      appId = StringUtils.substringBetween(servletPath, URL_CONFIGFILES_JSON_PREFIX, URL_SEPARATOR);
+    } else if (StringUtils.startsWith(servletPath, URL_CONFIGFILES_PREFIX)) {
+      appId = StringUtils.substringBetween(servletPath, URL_CONFIGFILES_PREFIX, URL_SEPARATOR);
+    } else if (StringUtils.startsWith(servletPath, URL_NOTIFICATIONS_PREFIX)) {
+      appId = request.getParameter("appId");
+    }
+
+    return appId;
+  }
+
+  public String buildSignature(String path, String query, String timestampString, String secret) {
+    String pathWithQuery = path;
+    if (!Strings.isNullOrEmpty(query)) {
+      pathWithQuery += "?" + query;
+    }
+
+    return Signature.signature(timestampString, pathWithQuery, secret);
+  }
+}

+ 111 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java

@@ -0,0 +1,111 @@
+package com.ctrip.framework.apollo.configservice.filter;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
+import com.ctrip.framework.apollo.core.signature.Signature;
+import com.google.common.collect.Lists;
+import java.util.List;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * @author nisiyong
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ClientAuthenticationFilterTest {
+
+  private ClientAuthenticationFilter clientAuthenticationFilter;
+
+  @Mock
+  private AccessKeyUtil accessKeyUtil;
+  @Mock
+  private HttpServletRequest request;
+  @Mock
+  private HttpServletResponse response;
+  @Mock
+  private FilterChain filterChain;
+
+  @Before
+  public void setUp() {
+    clientAuthenticationFilter = new ClientAuthenticationFilter(accessKeyUtil);
+  }
+
+  @Test
+  public void testInvalidAppId() throws Exception {
+    when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(null);
+
+    clientAuthenticationFilter.doFilter(request, response, filterChain);
+
+    verify(response).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId");
+    verify(filterChain, never()).doFilter(request, response);
+  }
+
+  @Test
+  public void testRequestTimeTooSkewed() throws Exception {
+    String appId = "someAppId";
+    List<String> secrets = Lists.newArrayList("someSecret");
+    String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000);
+
+    when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
+    when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets);
+    when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
+
+    clientAuthenticationFilter.doFilter(request, response, filterChain);
+
+    verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
+    verify(filterChain, never()).doFilter(request, response);
+  }
+
+  @Test
+  public void testUnauthorized() throws Exception {
+    String appId = "someAppId";
+    String availableSignature = "someSignature";
+    List<String> secrets = Lists.newArrayList("someSecret");
+    String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis());
+    String errorAuthorization = "Apollo someAppId:wrongSignature";
+
+    when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
+    when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets);
+    when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
+    when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
+    when(request.getHeader(Signature.HTTP_HEADER_AUTHORIZATION)).thenReturn(errorAuthorization);
+
+    clientAuthenticationFilter.doFilter(request, response, filterChain);
+
+    verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+    verify(filterChain, never()).doFilter(request, response);
+  }
+
+  @Test
+  public void testAuthorizedSuccessfully() throws Exception {
+    String appId = "someAppId";
+    String availableSignature = "someSignature";
+    List<String> secrets = Lists.newArrayList("someSecret");
+    String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis());
+    String correctAuthorization = "Apollo someAppId:someSignature";
+
+    when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
+    when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(secrets);
+    when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
+    when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
+    when(request.getHeader(Signature.HTTP_HEADER_AUTHORIZATION)).thenReturn(correctAuthorization);
+
+    clientAuthenticationFilter.doFilter(request, response, filterChain);
+
+    verify(response, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId");
+    verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
+    verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
+    verify(filterChain, times(1)).doFilter(request, response);
+  }
+}

+ 118 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCacheTest.java

@@ -0,0 +1,118 @@
+package com.ctrip.framework.apollo.configservice.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.biz.config.BizConfig;
+import com.ctrip.framework.apollo.biz.entity.AccessKey;
+import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository;
+import com.google.common.collect.Lists;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * @author nisiyong
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AccessKeyServiceWithCacheTest {
+
+  private AccessKeyServiceWithCache accessKeyServiceWithCache;
+  @Mock
+  private AccessKeyRepository accessKeyRepository;
+  @Mock
+  private BizConfig bizConfig;
+  private int scanInterval;
+  private TimeUnit scanIntervalTimeUnit;
+
+  @Before
+  public void setUp() {
+    accessKeyServiceWithCache = new AccessKeyServiceWithCache(accessKeyRepository, bizConfig);
+
+    scanInterval = 50;
+    scanIntervalTimeUnit = TimeUnit.MILLISECONDS;
+    when(bizConfig.accessKeyCacheScanInterval()).thenReturn(scanInterval);
+    when(bizConfig.accessKeyCacheScanIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit);
+    when(bizConfig.accessKeyCacheRebuildInterval()).thenReturn(scanInterval);
+    when(bizConfig.accessKeyCacheRebuildIntervalTimeUnit()).thenReturn(scanIntervalTimeUnit);
+  }
+
+  @Test
+  public void testGetAvailableSecrets() throws Exception {
+    String appId = "someAppId";
+    AccessKey firstAccessKey = assembleAccessKey(1L, appId, "secret-1", false,
+        false, 1577808000000L);
+    AccessKey secondAccessKey = assembleAccessKey(2L, appId, "secret-2", false,
+        false, 1577808001000L);
+    AccessKey thirdAccessKey = assembleAccessKey(3L, appId, "secret-3", true,
+        false, 1577808005000L);
+
+    // Initialize
+    accessKeyServiceWithCache.afterPropertiesSet();
+
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).isEmpty();
+
+    // Add access key, disable by default
+    when(accessKeyRepository.findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(new Date(0L)))
+        .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey));
+    when(accessKeyRepository.findAllById(anyList()))
+        .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey));
+
+    TimeUnit.SECONDS.sleep(1);
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).isEmpty();
+
+    // Update access key, enable both of them
+    firstAccessKey = assembleAccessKey(1L, appId, "secret-1", true, false, 1577808002000L);
+    secondAccessKey = assembleAccessKey(2L, appId, "secret-2", true, false, 1577808003000L);
+    when(accessKeyRepository.findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(new Date(1577808001000L)))
+        .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey));
+    when(accessKeyRepository.findAllById(anyList()))
+        .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey));
+
+    TimeUnit.SECONDS.sleep(1);
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).containsExactly("secret-1", "secret-2");
+
+    // Update access key, disable the first one
+    firstAccessKey = assembleAccessKey(1L, appId, "secret-1", false, false, 1577808004000L);
+    when(accessKeyRepository.findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(new Date(1577808003000L)))
+        .thenReturn(Lists.newArrayList(firstAccessKey));
+    when(accessKeyRepository.findAllById(anyList()))
+        .thenReturn(Lists.newArrayList(firstAccessKey, secondAccessKey));
+
+    TimeUnit.SECONDS.sleep(1);
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).containsExactly("secret-2");
+
+    // Delete access key, delete the second one
+    when(accessKeyRepository.findAllById(anyList()))
+        .thenReturn(Lists.newArrayList(firstAccessKey));
+
+    TimeUnit.SECONDS.sleep(1);
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).isEmpty();
+
+    // Add new access key in runtime, enable by default
+    when(accessKeyRepository.findFirst500ByDataChangeLastModifiedTimeGreaterThanOrderByDataChangeLastModifiedTimeAsc(new Date(1577808004000L)))
+        .thenReturn(Lists.newArrayList(thirdAccessKey));
+    when(accessKeyRepository.findAllById(anyList()))
+        .thenReturn(Lists.newArrayList(firstAccessKey, thirdAccessKey));
+
+    TimeUnit.SECONDS.sleep(1);
+    assertThat(accessKeyServiceWithCache.getAvailableSecrets(appId)).containsExactly("secret-3");
+  }
+
+  public AccessKey assembleAccessKey(Long id, String appId, String secret, boolean enabled,
+      boolean deleted, long dataChangeLastModifiedTime) {
+    AccessKey accessKey = new AccessKey();
+    accessKey.setId(id);
+    accessKey.setAppId(appId);
+    accessKey.setSecret(secret);
+    accessKey.setEnabled(enabled);
+    accessKey.setDeleted(deleted);
+    accessKey.setDataChangeLastModifiedTime(new Date(dataChangeLastModifiedTime));
+    return accessKey;
+  }
+}

+ 97 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtilTest.java

@@ -0,0 +1,97 @@
+package com.ctrip.framework.apollo.configservice.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.configservice.service.AccessKeyServiceWithCache;
+import com.google.common.collect.Lists;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * @author nisiyong
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AccessKeyUtilTest {
+
+  private AccessKeyUtil accessKeyUtil;
+
+  @Mock
+  private AccessKeyServiceWithCache accessKeyServiceWithCache;
+  @Mock
+  private HttpServletRequest request;
+
+  @Before
+  public void setUp() {
+    accessKeyUtil = new AccessKeyUtil(accessKeyServiceWithCache);
+  }
+
+  @Test
+  public void testFindAvailableSecret() {
+    String appId = "someAppId";
+    List<String> returnSecrets = Lists.newArrayList("someSecret");
+
+    when(accessKeyServiceWithCache.getAvailableSecrets(appId)).thenReturn(returnSecrets);
+
+    List<String> availableSecret = accessKeyUtil.findAvailableSecret(appId);
+
+    assertThat(availableSecret).containsExactly("someSecret");
+    verify(accessKeyServiceWithCache).getAvailableSecrets(appId);
+  }
+
+  @Test
+  public void testExtractAppIdFromRequest1() {
+    when(request.getServletPath()).thenReturn("/configs/someAppId/default/application");
+
+    String appId = accessKeyUtil.extractAppIdFromRequest(request);
+
+    assertThat(appId).isEqualTo("someAppId");
+  }
+
+  @Test
+  public void testExtractAppIdFromRequest2() {
+    when(request.getServletPath()).thenReturn("/configfiles/json/someAppId/default/application");
+
+    String appId = accessKeyUtil.extractAppIdFromRequest(request);
+
+    assertThat(appId).isEqualTo("someAppId");
+  }
+
+  @Test
+  public void testExtractAppIdFromRequest3() {
+    when(request.getServletPath()).thenReturn("/configfiles/someAppId/default/application");
+
+    String appId = accessKeyUtil.extractAppIdFromRequest(request);
+
+    assertThat(appId).isEqualTo("someAppId");
+  }
+
+  @Test
+  public void testExtractAppIdFromRequest4() {
+    when(request.getServletPath()).thenReturn("/notifications/v2");
+    when(request.getParameter("appId")).thenReturn("someAppId");
+
+    String appId = accessKeyUtil.extractAppIdFromRequest(request);
+
+    assertThat(appId).isEqualTo("someAppId");
+  }
+
+  @Test
+  public void buildSignature() {
+    String path = "/configs/someAppId/default/application";
+    String query = "ip=10.0.0.1";
+    String timestamp = "1575018989200";
+    String secret = "someSecret";
+
+    String actualSignature = accessKeyUtil.buildSignature(path, query, timestamp, secret);
+
+    String expectedSignature = "WYjjyJFei6DYiaMlwZjew2O/Yqk=";
+    assertThat(actualSignature).isEqualTo(expectedSignature);
+  }
+}

+ 31 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/HmacSha1Utils.java

@@ -0,0 +1,31 @@
+package com.ctrip.framework.apollo.core.signature;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author nisiyong
+ */
+public class HmacSha1Utils {
+
+  private static final String ALGORITHM_NAME = "HmacSHA1";
+  private static final String ENCODING = "UTF-8";
+
+  public static String signString(String stringToSign, String accessKeySecret) {
+    try {
+      Mac mac = Mac.getInstance(ALGORITHM_NAME);
+      mac.init(new SecretKeySpec(
+          accessKeySecret.getBytes(ENCODING),
+          ALGORITHM_NAME
+      ));
+      byte[] signData = mac.doFinal(stringToSign.getBytes(ENCODING));
+      return DatatypeConverter.printBase64Binary(signData);
+    } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException e) {
+      throw new IllegalArgumentException(e.toString());
+    }
+  }
+}

+ 55 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/signature/Signature.java

@@ -0,0 +1,55 @@
+package com.ctrip.framework.apollo.core.signature;
+
+import com.google.common.collect.Maps;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * @author nisiyong
+ */
+public class Signature {
+
+  /**
+   * Authorization=Apollo {appId}:{sign}
+   */
+  private static final String AUTHORIZATION_FORMAT = "Apollo %s:%s";
+  private static final String DELIMITER = "\n";
+
+  public static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
+  public static final String HTTP_HEADER_TIMESTAMP = "Timestamp";
+
+  public static String signature(String timestamp, String pathWithQuery, String secret) {
+    String stringToSign = timestamp + DELIMITER + pathWithQuery;
+    return HmacSha1Utils.signString(stringToSign, secret);
+  }
+
+  public static Map<String, String> buildHttpHeaders(String url, String appId, String secret) {
+    long currentTimeMillis = System.currentTimeMillis();
+    String timestamp = String.valueOf(currentTimeMillis);
+
+    String pathWithQuery = url2PathWithQuery(url);
+    String signature = signature(timestamp, pathWithQuery, secret);
+
+    Map<String, String> headers = Maps.newHashMap();
+    headers.put(HTTP_HEADER_AUTHORIZATION, String.format(AUTHORIZATION_FORMAT, appId, signature));
+    headers.put(HTTP_HEADER_TIMESTAMP, timestamp);
+    return headers;
+  }
+
+  private static String url2PathWithQuery(String urlString) {
+    try {
+      URL url = new URL(urlString);
+      String path = url.getPath();
+      String query = url.getQuery();
+
+      String pathWithQuery = path;
+      if (query != null && query.length() > 0) {
+        pathWithQuery += "?" + query;
+      }
+      return pathWithQuery;
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException("Invalid url pattern: " + urlString, e);
+    }
+  }
+}

+ 55 - 5
apollo-core/src/main/java/com/ctrip/framework/foundation/internals/provider/DefaultApplicationProvider.java

@@ -14,16 +14,19 @@ import com.ctrip.framework.foundation.spi.provider.ApplicationProvider;
 import com.ctrip.framework.foundation.spi.provider.Provider;
 
 public class DefaultApplicationProvider implements ApplicationProvider {
+
   private static final Logger logger = LoggerFactory.getLogger(DefaultApplicationProvider.class);
   public static final String APP_PROPERTIES_CLASSPATH = "/META-INF/app.properties";
   private Properties m_appProperties = new Properties();
 
   private String m_appId;
+  private String accessKeySecret;
 
   @Override
   public void initialize() {
     try {
-      InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(APP_PROPERTIES_CLASSPATH.substring(1));
+      InputStream in = Thread.currentThread().getContextClassLoader()
+          .getResourceAsStream(APP_PROPERTIES_CLASSPATH.substring(1));
       if (in == null) {
         in = DefaultApplicationProvider.class.getResourceAsStream(APP_PROPERTIES_CLASSPATH);
       }
@@ -39,13 +42,15 @@ public class DefaultApplicationProvider implements ApplicationProvider {
     try {
       if (in != null) {
         try {
-          m_appProperties.load(new InputStreamReader(new BOMInputStream(in), StandardCharsets.UTF_8));
+          m_appProperties
+              .load(new InputStreamReader(new BOMInputStream(in), StandardCharsets.UTF_8));
         } finally {
           in.close();
         }
       }
 
       initAppId();
+      initAccessKey();
     } catch (Throwable ex) {
       logger.error("Initialize DefaultApplicationProvider failed.", ex);
     }
@@ -56,6 +61,11 @@ public class DefaultApplicationProvider implements ApplicationProvider {
     return m_appId;
   }
 
+  @Override
+  public String getAccessKeySecret() {
+    return accessKeySecret;
+  }
+
   @Override
   public boolean isAppIdSet() {
     return !Utils.isBlank(m_appId);
@@ -67,6 +77,12 @@ public class DefaultApplicationProvider implements ApplicationProvider {
       String val = getAppId();
       return val == null ? defaultValue : val;
     }
+
+    if ("apollo.accesskey.secret".equals(name)) {
+      String val = getAccessKeySecret();
+      return val == null ? defaultValue : val;
+    }
+
     String val = m_appProperties.getProperty(name, defaultValue);
     return val == null ? defaultValue : val;
   }
@@ -97,16 +113,50 @@ public class DefaultApplicationProvider implements ApplicationProvider {
     m_appId = m_appProperties.getProperty("app.id");
     if (!Utils.isBlank(m_appId)) {
       m_appId = m_appId.trim();
-      logger.info("App ID is set to {} by app.id property from {}", m_appId, APP_PROPERTIES_CLASSPATH);
+      logger.info("App ID is set to {} by app.id property from {}", m_appId,
+          APP_PROPERTIES_CLASSPATH);
       return;
     }
 
     m_appId = null;
-    logger.warn("app.id is not available from System Property and {}. It is set to null", APP_PROPERTIES_CLASSPATH);
+    logger.warn("app.id is not available from System Property and {}. It is set to null",
+        APP_PROPERTIES_CLASSPATH);
+  }
+
+  private void initAccessKey() {
+    // 1. Get accesskey secret from System Property
+    accessKeySecret = System.getProperty("apollo.accesskey.secret");
+    if (!Utils.isBlank(accessKeySecret)) {
+      accessKeySecret = accessKeySecret.trim();
+      logger
+          .info("ACCESSKEY SECRET is set by apollo.accesskey.secret property from System Property");
+      return;
+    }
+
+    //2. Try to get accesskey secret from OS environment variable
+    accessKeySecret = System.getenv("APOLLO_ACCESSKEY_SECRET");
+    if (!Utils.isBlank(accessKeySecret)) {
+      accessKeySecret = accessKeySecret.trim();
+      logger.info(
+          "ACCESSKEY SECRET is set by APOLLO_ACCESSKEY_SECRET property from OS environment variable");
+      return;
+    }
+
+    // 3. Try to get accesskey secret from app.properties.
+    accessKeySecret = m_appProperties.getProperty("apollo.accesskey.secret");
+    if (!Utils.isBlank(accessKeySecret)) {
+      accessKeySecret = accessKeySecret.trim();
+      logger.info("ACCESSKEY SECRET is set by apollo.accesskey.secret property from {}",
+          APP_PROPERTIES_CLASSPATH);
+      return;
+    }
+
+    accessKeySecret = null;
   }
 
   @Override
   public String toString() {
-    return "appId [" + getAppId() + "] properties: " + m_appProperties + " (DefaultApplicationProvider)";
+    return "appId [" + getAppId() + "] properties: " + m_appProperties
+        + " (DefaultApplicationProvider)";
   }
 }

+ 5 - 0
apollo-core/src/main/java/com/ctrip/framework/foundation/internals/provider/NullProvider.java

@@ -28,6 +28,11 @@ public class NullProvider implements ApplicationProvider, NetworkProvider, Serve
     return null;
   }
 
+  @Override
+  public String getAccessKeySecret() {
+    return null;
+  }
+
   @Override
   public boolean isAppIdSet() {
     return false;

+ 5 - 0
apollo-core/src/main/java/com/ctrip/framework/foundation/spi/provider/ApplicationProvider.java

@@ -11,6 +11,11 @@ public interface ApplicationProvider extends Provider {
    */
   public String getAppId();
 
+  /**
+   * @return the application's access key secret
+   */
+  public String getAccessKeySecret();
+
   /**
    * @return whether the application's app id is set or not
    */

+ 22 - 0
apollo-core/src/test/java/com/ctrip/framework/apollo/core/signature/HmacSha1UtilsTest.java

@@ -0,0 +1,22 @@
+package com.ctrip.framework.apollo.core.signature;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+/**
+ * @author nisiyong
+ */
+public class HmacSha1UtilsTest {
+
+  @Test
+  public void testSignString() {
+    String stringToSign = "1576478257344\n/configs/100004458/default/application?ip=10.0.0.1";
+    String accessKeySecret = "df23df3f59884980844ff3dada30fa97";
+
+    String actualSignature = HmacSha1Utils.signString(stringToSign, accessKeySecret);
+
+    String expectedSignature = "EoKyziXvKqzHgwx+ijDJwgVTDgE=";
+    assertThat(actualSignature).isEqualTo(expectedSignature);
+  }
+}

+ 36 - 0
apollo-core/src/test/java/com/ctrip/framework/apollo/core/signature/SignatureTest.java

@@ -0,0 +1,36 @@
+package com.ctrip.framework.apollo.core.signature;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.junit.Test;
+
+/**
+ * @author nisiyong
+ */
+public class SignatureTest {
+
+  @Test
+  public void testSignature() {
+    String timestamp = "1576478257344";
+    String pathWithQuery = "/configs/100004458/default/application?ip=10.0.0.1";
+    String secret = "df23df3f59884980844ff3dada30fa97";
+
+    String actualSignature = Signature.signature(timestamp, pathWithQuery, secret);
+
+    String expectedSignature = "EoKyziXvKqzHgwx+ijDJwgVTDgE=";
+    assertThat(actualSignature).isEqualTo(expectedSignature);
+  }
+
+  @Test
+  public void testBuildHttpHeaders() {
+    String url = "http://10.0.0.1:8080/configs/100004458/default/application?ip=10.0.0.1";
+    String appId = "100004458";
+    String secret = "df23df3f59884980844ff3dada30fa97";
+
+    Map<String, String> actualHttpHeaders = Signature.buildHttpHeaders(url, appId, secret);
+
+    assertThat(actualHttpHeaders)
+        .containsKeys(Signature.HTTP_HEADER_AUTHORIZATION, Signature.HTTP_HEADER_TIMESTAMP);
+  }
+}

+ 4 - 3
apollo-core/src/test/java/com/ctrip/framework/foundation/internals/provider/DefaultApplicationProviderTest.java

@@ -6,12 +6,9 @@ import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.FileInputStream;
-
 import org.junit.Before;
 import org.junit.Test;
 
-import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider;
-
 public class DefaultApplicationProviderTest {
   private DefaultApplicationProvider defaultApplicationProvider;
   String PREDEFINED_APP_ID = "110402";
@@ -43,12 +40,16 @@ public class DefaultApplicationProviderTest {
   @Test
   public void testLoadAppPropertiesWithSystemProperty() throws Exception {
     String someAppId = "someAppId";
+    String someSecret = "someSecret";
     System.setProperty("app.id", someAppId);
+    System.setProperty("apollo.accesskey.secret", someSecret);
     defaultApplicationProvider.initialize();
     System.clearProperty("app.id");
+    System.clearProperty("apollo.accesskey.secret");
 
     assertEquals(someAppId, defaultApplicationProvider.getAppId());
     assertTrue(defaultApplicationProvider.isAppIdSet());
+    assertEquals(someSecret, defaultApplicationProvider.getAccessKeySecret());
   }
 
   @Test

+ 31 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java

@@ -1,6 +1,7 @@
 package com.ctrip.framework.apollo.portal.api;
 
 
+import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
 import com.ctrip.framework.apollo.common.dto.AppDTO;
 import com.ctrip.framework.apollo.common.dto.AppNamespaceDTO;
 import com.ctrip.framework.apollo.common.dto.ClusterDTO;
@@ -230,6 +231,36 @@ public class AdminServiceAPI {
     }
   }
 
+  @Service
+  public static class AccessKeyAPI extends API {
+
+    public AccessKeyDTO create(Env env, AccessKeyDTO accessKey) {
+      return restTemplate.post(env, "apps/{appId}/accesskeys",
+          accessKey, AccessKeyDTO.class, accessKey.getAppId());
+    }
+
+    public List<AccessKeyDTO> findByAppId(Env env, String appId) {
+      AccessKeyDTO[] accessKeys = restTemplate.get(env, "apps/{appId}/accesskeys",
+          AccessKeyDTO[].class, appId);
+      return Arrays.asList(accessKeys);
+    }
+
+    public void delete(Env env, String appId, long id, String operator) {
+      restTemplate.delete(env, "apps/{appId}/accesskeys/{id}?operator={operator}",
+          appId, id, operator);
+    }
+
+    public void enable(Env env, String appId, long id, String operator) {
+      restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?operator={operator}",
+          null, appId, id, operator);
+    }
+
+    public void disable(Env env, String appId, long id, String operator) {
+      restTemplate.put(env, "apps/{appId}/accesskeys/{id}/disable?operator={operator}",
+          null, appId, id, operator);
+    }
+  }
+
   @Service
   public static class ReleaseAPI extends API {
 

+ 2 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/TracerEventType.java

@@ -14,6 +14,8 @@ public interface TracerEventType {
 
   String CREATE_CLUSTER = "Cluster.Create";
 
+  String CREATE_ACCESS_KEY = "AccessKey.Create";
+
   String CREATE_NAMESPACE = "Namespace.Create";
 
   String API_RETRY = "API.Retry";

+ 77 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AccessKeyController.java

@@ -0,0 +1,77 @@
+package com.ctrip.framework.apollo.portal.controller;
+
+import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.portal.service.AccessKeyService;
+import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
+import java.util.List;
+import java.util.UUID;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author nisiyong
+ */
+@RestController
+public class AccessKeyController {
+
+  private final UserInfoHolder userInfoHolder;
+  private final AccessKeyService accessKeyService;
+
+  public AccessKeyController(
+      UserInfoHolder userInfoHolder,
+      AccessKeyService accessKeyService) {
+    this.userInfoHolder = userInfoHolder;
+    this.accessKeyService = accessKeyService;
+  }
+
+  @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
+  @PostMapping(value = "/apps/{appId}/envs/{env}/accesskeys")
+  public AccessKeyDTO save(@PathVariable String appId, @PathVariable String env,
+      @RequestBody AccessKeyDTO accessKeyDTO) {
+    String secret = UUID.randomUUID().toString().replaceAll("-", "");
+    accessKeyDTO.setAppId(appId);
+    accessKeyDTO.setSecret(secret);
+    return accessKeyService.createAccessKey(Env.fromString(env), accessKeyDTO);
+  }
+
+  @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
+  @GetMapping(value = "/apps/{appId}/envs/{env}/accesskeys")
+  public List<AccessKeyDTO> findByAppId(@PathVariable String appId,
+      @PathVariable String env) {
+    return accessKeyService.findByAppId(Env.fromString(env), appId);
+  }
+
+  @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
+  @DeleteMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}")
+  public void delete(@PathVariable String appId,
+      @PathVariable String env,
+      @PathVariable long id) {
+    String operator = userInfoHolder.getUser().getUserId();
+    accessKeyService.deleteAccessKey(Env.fromString(env), appId, id, operator);
+  }
+
+  @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
+  @PutMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}/enable")
+  public void enable(@PathVariable String appId,
+      @PathVariable String env,
+      @PathVariable long id) {
+    String operator = userInfoHolder.getUser().getUserId();
+    accessKeyService.enable(Env.fromString(env), appId, id, operator);
+  }
+
+  @PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
+  @PutMapping(value = "/apps/{appId}/envs/{env}/accesskeys/{id}/disable")
+  public void disable(@PathVariable String appId,
+      @PathVariable String env,
+      @PathVariable long id) {
+    String operator = userInfoHolder.getUser().getUserId();
+    accessKeyService.disable(Env.fromString(env), appId, id, operator);
+  }
+}

+ 42 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AccessKeyService.java

@@ -0,0 +1,42 @@
+package com.ctrip.framework.apollo.portal.service;
+
+import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.AccessKeyAPI;
+import com.ctrip.framework.apollo.portal.constant.TracerEventType;
+import com.ctrip.framework.apollo.tracer.Tracer;
+import java.util.List;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AccessKeyService {
+
+  private final AdminServiceAPI.AccessKeyAPI accessKeyAPI;
+
+  public AccessKeyService(AccessKeyAPI accessKeyAPI) {
+    this.accessKeyAPI = accessKeyAPI;
+  }
+
+  public List<AccessKeyDTO> findByAppId(Env env, String appId) {
+    return accessKeyAPI.findByAppId(env, appId);
+  }
+
+  public AccessKeyDTO createAccessKey(Env env, AccessKeyDTO accessKey) {
+    AccessKeyDTO accessKeyDTO = accessKeyAPI.create(env, accessKey);
+    Tracer.logEvent(TracerEventType.CREATE_ACCESS_KEY, accessKey.getAppId());
+    return accessKeyDTO;
+  }
+
+  public void deleteAccessKey(Env env, String appId, long id, String operator) {
+    accessKeyAPI.delete(env, appId, id, operator);
+  }
+
+  public void enable(Env env, String appId, long id, String operator) {
+    accessKeyAPI.enable(env, appId, id, operator);
+  }
+
+  public void disable(Env env, String appId, long id, String operator) {
+    accessKeyAPI.disable(env, appId, id, operator);
+  }
+}

+ 43 - 45
apollo-portal/src/main/resources/static/app/access_key.html

@@ -37,28 +37,32 @@
             <div class="panel-body row">
 
                 <section class="context" ng-show="hasAssignUserPermission">
-                    <!--project admin-->
-                    <section class="form-horizontal" ng-show="hasManageAppMasterPermission">
-                        <h5>{{'App.Setting.Admin' | translate }}
-                            <small>
-                                {{'App.AccessKey.AdminTips' | translate }}
-                            </small>
-                        </h5>
-                        <hr>
-
+                    <section class="form-horizontal">
+                        <div class="alert alert-info no-radius" role="alert">
+                            <strong>Tips:</strong>
+                            <ul>
+                                <li>{{'AccessKey.Tips.1' | translate }}</li>
+                                <li>{{'AccessKey.Tips.2' | translate }}</li>
+                                <li>{{'AccessKey.Tips.3' | translate }}</li>
+                                <ul>
+                                    <li>{{'AccessKey.Tips.3.1' | translate }}</li>
+                                    <li>{{'AccessKey.Tips.3.2' | translate }}</li>
+                                    <li>{{'AccessKey.Tips.3.3' | translate }}</li>
+                                </ul>
+                            </ul>
+                        </div>
+                    </section>
+                    <section>
                         <div class="row">
-                            <label class="col-sm-1 control-label" style="width: 150px;">
-                                {{'AccessKey.AddButton.Label' | translate }}
-                            </label>
                             <form class="col-sm-8 form-inline" ng-submit="create()">
-                                <div class="form-group" style="padding-left: 15px">
+                                <div class="form-group">
                                     <select class="form-control input-sm" style="width: 450px;" ng-model="addAccessKeySelectedEnv">
                                         <option value="">{{'Cluster.PleaseChooseEnvironment' | translate }}</option>
                                         <option ng-repeat="env in envs" ng-value="env">{{env}}</option>
                                     </select>
                                 </div>
                                 <button type="submit" class="btn btn-default" style="margin-left: 20px;"
-                                    ng-disabled="addAccessKeySelectedEnv == ''">{{'App.Setting.Add' | translate }}
+                                        ng-disabled="addAccessKeySelectedEnv == ''">{{'App.Setting.Add' | translate }}
                                 </button>
                             </form>
                         </div>
@@ -72,42 +76,36 @@
 
                         <table class="table table-striped table-hover table-bordered">
                             <thead>
-                            <tr>
-                                <th>{{'AccessKey.ConfigAccessKeys.Secret' | translate }}</th>
-                                <th>{{'AccessKey.ConfigAccessKeys.Status' | translate }}</th>
-                              <th>{{'AccessKey.ConfigAccessKeys.LastModify' | translate }}</th>
-                              <th>{{'AccessKey.ConfigAccessKeys.LastModifyTime' | translate }}</th>
-                                <th>{{'AccessKey.ConfigAccessKeys.Operator' | translate }}</th>
-                            </tr>
+                                <tr>
+                                    <th>{{'AccessKey.ConfigAccessKeys.Secret' | translate }}</th>
+                                    <th>{{'AccessKey.ConfigAccessKeys.Status' | translate }}</th>
+                                    <th>{{'AccessKey.ConfigAccessKeys.LastModify' | translate }}</th>
+                                    <th>{{'AccessKey.ConfigAccessKeys.LastModifyTime' | translate }}</th>
+                                    <th>{{'AccessKey.ConfigAccessKeys.Operator' | translate }}</th>
+                                </tr>
                             </thead>
                             <tbody>
-                            <tr ng-show="(!accessKeys[env] || accessKeys[env].length < 1)">
-                                <td colspan="4">{{'AccessKey.NoAccessKeyServiceTips' | translate }}</td>
-                            </tr>
-                            <tr ng-show="accessKeys[env] && accessKeys[env].length > 0"
-                                ng-repeat="accessKey in accessKeys[env]">
-                                <td style="text-align: center;">{{accessKey.secret}}</td>
-                                <td style="text-align: center;" ng-style="{'color': accessKey.enabled ? '#5cb85c' : '#d20707'}">{{accessKey.enabled ? ('AccessKey.Operator.Enabled' | translate) : ('AccessKey.Operator.Disabled' | translate) }}</td>
-                              <td style="text-align: center;">
-                                {{accessKey.dataChangeLastModifiedBy}}
-                              </td>
-                              <td style="text-align: center;">{{accessKey.dataChangeLastModifiedTime
-                                | date: 'yyyy-MM-dd HH:mm:ss'}}
-                              </td>
-                                <td style="text-align: center;">
-                                    <a href="javascript:;"
-                                       ng-click="enable(accessKey.id, env)" ng-if="!accessKey.enabled">{{'AccessKey.Operator.Enable' | translate}}</a>
-                                    <a href="javascript:;"
-                                       ng-click="disable(accessKey.id, env)" ng-if="accessKey.enabled">{{'AccessKey.Operator.Disable' | translate}}</a>
-                                    <a href="javascript:;"
-                                       ng-click="remove(accessKey.id, env)">{{'AccessKey.Operator.Remove' | translate }}</a>
-                                </td>
-                            </tr>
+                                <tr ng-show="(!accessKeys[env] || accessKeys[env].length < 1)">
+                                    <td colspan="5" style="text-align: center;">{{'AccessKey.NoAccessKeyServiceTips' | translate }}</td>
+                                </tr>
+                                <tr ng-show="accessKeys[env] && accessKeys[env].length > 0"
+                                    ng-repeat="accessKey in accessKeys[env]">
+                                    <td style="text-align: center;">{{accessKey.secret}}</td>
+                                    <td style="text-align: center;" ng-style="{'color': accessKey.enabled ? '#5cb85c' : '#d20707'}">{{accessKey.enabled ? ('AccessKey.Operator.Enabled' | translate) : ('AccessKey.Operator.Disabled' | translate) }}</td>
+                                    <td style="text-align: center;">{{accessKey.dataChangeLastModifiedBy}}</td>
+                                    <td style="text-align: center;">{{accessKey.dataChangeLastModifiedTime | date: 'yyyy-MM-dd HH:mm:ss'}}</td>
+                                    <td style="text-align: center;">
+                                        <a href="javascript:;"
+                                           ng-click="enable(accessKey.id, env)" ng-if="!accessKey.enabled">{{'AccessKey.Operator.Enable' | translate}}</a>
+                                        <a href="javascript:;"
+                                           ng-click="disable(accessKey.id, env)" ng-if="accessKey.enabled">{{'AccessKey.Operator.Disable' | translate}}</a>
+                                        <a href="javascript:;"
+                                           ng-click="remove(accessKey.id, env)" ng-if="!accessKey.enabled">{{'AccessKey.Operator.Remove' | translate }}</a>
+                                    </td>
+                                </tr>
                             </tbody>
                         </table>
                     </section>
-
-
                 </section>
 
                 <section class="context" ng-show="!hasAssignUserPermission">

+ 2 - 3
apollo-portal/src/main/resources/static/config.html

@@ -121,9 +121,8 @@
                             apollo-href="'app/setting.html?#/appid=' + pageContext.appId"></apolloentrance>
 
                         <apolloentrance apollo-title="'Config.AccessKeyManage' | translate"
-                                        apollo-img-src="'project-manage'"
-                                        apollo-href="'/app/access_key.html?#/appid=' + pageContext.appId"
-                                        ng-show="hasCreateClusterPermission"></apolloentrance>
+                                        apollo-img-src="'accesskey-manage'"
+                                        apollo-href="'/app/access_key.html?#/appid=' + pageContext.appId"></apolloentrance>
 
                         <a class="list-group-item" ng-show="missEnvs.length > 0" ng-click="createAppInMissEnv()">
                             <div class="row icon-text icon-plus-orange">

+ 20 - 16
apollo-portal/src/main/resources/static/i18n/en.json

@@ -464,9 +464,14 @@
   "ServiceConfig.PleaseEnterKey": "Please enter key",
   "ServiceConfig.KeyNotExistsAndCreateTip": "Key: '{{key}}' does not exist. Click Save to create the configuration item.",
   "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' already exists. Click Save will override the configuration item.",
-  "AccessKey.AddButton.Label": "Create AccessKey",
-  "AccessKey.NoAccessKeyServiceTips": "No accessKey found!",
-  "AccessKey.ConfigAccessKeys.Secret": "AccessKey Secret",
+  "AccessKey.Tips.1": "Add up to 5 access keys per environment.",
+  "AccessKey.Tips.2": "Once the environment has any enabled access key, the client will be required to configure access key, or the configurations cannot be obtained.",
+  "AccessKey.Tips.3": "Configure the access key to prevent unauthorized clients from obtaining the application configuration. The configuration method is as follows:",
+  "AccessKey.Tips.3.1": "Via jvm parameter -Dapollo.accesskey.secret",
+  "AccessKey.Tips.3.2": "Through the environment variable APOLLO_ACCESSKEY_SECRET",
+  "AccessKey.Tips.3.3": "Configure apollo.accesskey.secret via META-INF/app.properties or application.properties (note that the multi-environment secret is different)",
+  "AccessKey.NoAccessKeyServiceTips": "There are no access keys in this environment.",
+  "AccessKey.ConfigAccessKeys.Secret": "Access Key Secret",
   "AccessKey.ConfigAccessKeys.Status": "Status",
   "AccessKey.ConfigAccessKeys.LastModify": "Last Modifier",
   "AccessKey.ConfigAccessKeys.LastModifyTime": "Last Modified Time",
@@ -476,18 +481,18 @@
   "AccessKey.Operator.Disabled": "Disabled",
   "AccessKey.Operator.Enabled": "Enabled",
   "AccessKey.Operator.Remove": "Remove",
-  "AccessKey.Operator.CreateSuccess": "Create accessKey for '{{env}}' success",
-  "AccessKey.Operator.DisabledSuccess": "Disabled accessKey for '{{env}}' success",
-  "AccessKey.Operator.EnabledSuccess": "Enabled accessKey for '{{env}}' success",
-  "AccessKey.Operator.RemoveSuccess": "Remove accessKey for '{{env}}' success",
-  "AccessKey.Operator.CreateError": "Failed to create accessKey for '{{env}}'",
-  "AccessKey.Operator.DisabledError": "Failed to disabled accessKey for '{{env}}'",
-  "AccessKey.Operator.EnabledError": "Failed to enabled accessKey for '{{env}}'",
-  "AccessKey.Operator.RemoveError": "Failed to remove accessKey for '{{env}}'",
-  "AccessKey.Operator.DisabledTips": "Are you sure disable {{env}} AccessKey?",
-  "AccessKey.Operator.EnabledTips": "Are you sure enable {{env}} AccessKey?",
-  "AccessKey.Operator.RemoveTips": "Are you sure remove {{env}} AccessKey?",
-  "AccessKey.LoadError": "Failed to load accessKeys for '{{env}}'",
+  "AccessKey.Operator.CreateSuccess": "Access key created successfully",
+  "AccessKey.Operator.DisabledSuccess": "Access key disabled successfully",
+  "AccessKey.Operator.EnabledSuccess": "Access key enabled successfully",
+  "AccessKey.Operator.RemoveSuccess": "Access key removed successfully",
+  "AccessKey.Operator.CreateError": "Access key created failed",
+  "AccessKey.Operator.DisabledError": "Access key disabled failed",
+  "AccessKey.Operator.EnabledError": "Access key enabled failed",
+  "AccessKey.Operator.RemoveError": "Access key removed failed",
+  "AccessKey.Operator.DisabledTips": "Are you sure you want to disable the access key?",
+  "AccessKey.Operator.EnabledTips": "Are you sure you want to enable the access key?",
+  "AccessKey.Operator.RemoveTips": "Are you sure you want to remove the access key?",
+  "AccessKey.LoadError": "Error Loading access keys",
   "SystemInfo.Title": "System Information",
   "SystemInfo.SystemVersion": "System version",
   "SystemInfo.Tips1": "The environment list comes from the <strong>apollo.portal.envs</strong> configuration in Apollo PortalDB.ServerConfig, and can be configured in <a href=\"{{serverConfigUrl}}\">System Configuration</a> page. For more information, please refer the <strong>apollo.portal.envs - supportable environment list</strong> section in <a href=\"{{wikiUrl}}\">Distributed Deployment Guide</a>.",
@@ -674,7 +679,6 @@
   "App.AppOwnerTips": "(After enabling the application administrator allocation restrictions, the application owner and project administrator are default to current account, not subject to change)",
   "App.AppAdminTips1": "(The application owner has project administrator permission by default.",
   "App.AppAdminTips2": "Project administrators can create namespace, cluster, and assign user permissions)",
-  "App.AccessKey.AdminTips": "(Project administrators can config accessKey)",
   "App.AccessKey.NoPermissionTips": "You do not have permission to operate, please ask [{{users}}] to authorize",
   "App.Setting.Title": "Manage Project",
   "App.Setting.Admin": "Administrators",

+ 20 - 16
apollo-portal/src/main/resources/static/i18n/zh-CN.json

@@ -464,9 +464,14 @@
   "ServiceConfig.PleaseEnterKey": "请输入key",
   "ServiceConfig.KeyNotExistsAndCreateTip": "Key: '{{key}}' 不存在,点击保存后会创建该配置项",
   "ServiceConfig.KeyExistsAndSaveTip": "Key: '{{key}}' 已存在,点击保存后会覆盖该配置项",
-  "AccessKey.AddButton.Label": "创建AccessKey",
-  "AccessKey.NoAccessKeyServiceTips": "No accessKey found!",
-  "AccessKey.ConfigAccessKeys.Secret": "AccessKey Secret",
+  "AccessKey.Tips.1": "每个环境最多可添加5个访问密钥",
+  "AccessKey.Tips.2": "一旦该环境有启用的访问密钥,客户端将被要求配置密钥,否则无法获取配置",
+  "AccessKey.Tips.3": "配置访问密钥防止非法客户端获取护应用配置,配置方式如下:",
+  "AccessKey.Tips.3.1": "通过jvm参数-Dapollo.accesskey.secret",
+  "AccessKey.Tips.3.2": "通过环境变量APOLLO_ACCESSKEY_SECRET",
+  "AccessKey.Tips.3.3": "通过META-INF/app.properties或application.properties配置apollo.accesskey.secret(注意多环境secret不一样)",
+  "AccessKey.NoAccessKeyServiceTips": "该环境没有配置访问密钥",
+  "AccessKey.ConfigAccessKeys.Secret": "访问密钥",
   "AccessKey.ConfigAccessKeys.Status": "状态",
   "AccessKey.ConfigAccessKeys.LastModify": "最后修改人",
   "AccessKey.ConfigAccessKeys.LastModifyTime": "最后修改时间",
@@ -476,18 +481,18 @@
   "AccessKey.Operator.Disabled": "已禁用",
   "AccessKey.Operator.Enabled": "已启用",
   "AccessKey.Operator.Remove": "删除",
-  "AccessKey.Operator.CreateSuccess": " {{env}}环境AccessKey创建成功",
-  "AccessKey.Operator.DisabledSuccess": " {{env}}环境AccessKey禁用成功",
-  "AccessKey.Operator.EnabledSuccess": " {{env}}环境AccessKey启用成功",
-  "AccessKey.Operator.RemoveSuccess": " {{env}}环境AccessKey删除成功",
-  "AccessKey.Operator.CreateError": " {{env}}环境AccessKey创建失败",
-  "AccessKey.Operator.DisabledError": " {{env}}环境AccessKey禁用失败",
-  "AccessKey.Operator.EnabledError": " {{env}}环境AccessKey启用失败",
-  "AccessKey.Operator.RemoveError": " {{env}}环境AccessKey删除失败",
-  "AccessKey.Operator.DisabledTips": "是否确定禁用{{env}}AccessKey?",
-  "AccessKey.Operator.EnabledTips": " 是否确定启用{{env}}AccessKey?",
-  "AccessKey.Operator.RemoveTips": " 是否确定删除{{env}}AccessKey?",
-  "AccessKey.LoadError": "加载 '{{env}}' AccessKeys出错",
+  "AccessKey.Operator.CreateSuccess": "访问密钥创建成功",
+  "AccessKey.Operator.DisabledSuccess": "访问密钥禁用成功",
+  "AccessKey.Operator.EnabledSuccess": "访问密钥启用成功",
+  "AccessKey.Operator.RemoveSuccess": "访问密钥删除成功",
+  "AccessKey.Operator.CreateError": "访问密钥创建失败",
+  "AccessKey.Operator.DisabledError": "访问密钥禁用失败",
+  "AccessKey.Operator.EnabledError": "访问密钥启用失败",
+  "AccessKey.Operator.RemoveError": "访问密钥删除失败",
+  "AccessKey.Operator.DisabledTips": "是否确定禁用该访问密钥?",
+  "AccessKey.Operator.EnabledTips": " 是否确定启用该访问密钥?",
+  "AccessKey.Operator.RemoveTips": " 是否确定删除该访问密钥?",
+  "AccessKey.LoadError": "加载访问密钥出错",
   "SystemInfo.Title": "系统信息",
   "SystemInfo.SystemVersion": "系统版本",
   "SystemInfo.Tips1": "环境列表来自于ApolloPortalDB.ServerConfig中的<strong>apollo.portal.envs</strong>配置,可以到<a href=\"{{serverConfigUrl}}\">系统参数</a>页面配置,更多信息可以参考<a href=\"{{wikiUrl}}\">分布式部署指南</a>中的<strong>apollo.portal.envs - 可支持的环境列表</strong>章节。",
@@ -674,7 +679,6 @@
   "App.AppOwnerTips": "(开启项目管理员分配权限控制后,应用负责人和项目管理员默认为本账号,不可选择)",
   "App.AppAdminTips1": "(应用负责人默认具有项目管理员权限,",
   "App.AppAdminTips2": "项目管理员可以创建Namespace和集群、分配用户权限)",
-  "App.AccessKey.AdminTips": "(项目管理员可以配置秘钥)",
   "App.AccessKey.NoPermissionTips": "您没有权限操作,请找 [{{users}}] 开通权限",
   "App.Setting.Title": "项目管理",
   "App.Setting.Admin": "管理员",

BIN
apollo-portal/src/main/resources/static/img/secret.png


+ 30 - 30
apollo-portal/src/main/resources/static/scripts/controller/AccessKeyController.js

@@ -33,49 +33,30 @@ function AccessKeyController($scope, $location, $translate, toastr,
     init();
 
     function init() {
-        initEnv();
         initPermission();
+        initAdmins();
         initApplication();
     }
 
-    function initEnv() {
-        EnvService.find_all_envs()
-            .then(function (result) {
-                $scope.envs = result;
-                initAccessKeys();
-            });
-    }
-
     function initPermission() {
         PermissionService.has_assign_user_permission($scope.pageContext.appId)
             .then(function (result) {
                 $scope.hasAssignUserPermission = result.hasPermission;
 
-                PermissionService.has_open_manage_app_master_role_limit().then(function (value) {
-                    if (!value.isManageAppMasterPermissionEnabled) {
-                        $scope.hasManageAppMasterPermission = $scope.hasAssignUserPermission;
-                        return;
-                    }
-
-                    PermissionService.has_manage_app_master_permission($scope.pageContext.appId).then(function (res) {
-                        $scope.hasManageAppMasterPermission = res.hasPermission && $scope.hasAssignUserPermission;
-
-                        PermissionService.has_root_permission().then(function (value) {
-                            $scope.hasManageAppMasterPermission = value.hasPermission || $scope.hasManageAppMasterPermission;
-                        });
-                    });
-                });
+                if (result.hasPermission) {
+                    initEnv();
+                }
             });
     }
 
-    function initApplication() {
-        AppService.load($scope.pageContext.appId).then(function (app) {
-            $scope.app = app;
-            $scope.viewApp = _.clone(app);
-            $('.project-setting .panel').removeClass('hidden');
-        })
+    function initEnv() {
+        EnvService.find_all_envs()
+        .then(function (result) {
+            $scope.envs = result;
+            initAccessKeys();
+        });
     }
-    
+
     function initAccessKeys() {
         $scope.accessKeys = {};
         for (var iLoop = 0; iLoop < $scope.envs.length; iLoop++) {
@@ -92,6 +73,25 @@ function AccessKeyController($scope, $location, $translate, toastr,
             });
     }
 
+    function initAdmins() {
+        PermissionService.get_app_role_users($scope.pageContext.appId)
+        .then(function (result) {
+            $scope.appRoleUsers = result;
+            $scope.admins = [];
+            $scope.appRoleUsers.masterUsers.forEach(function (user) {
+                $scope.admins.push(user.userId);
+            });
+        });
+    }
+
+    function initApplication() {
+        AppService.load($scope.pageContext.appId).then(function (app) {
+            $scope.app = app;
+            $scope.viewApp = _.clone(app);
+            $('.project-setting .panel').removeClass('hidden');
+        })
+    }
+
     function create() {
         var env = $scope.addAccessKeySelectedEnv;
         UserService.load_user().then(function (result) {

+ 1 - 7
apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js

@@ -345,19 +345,13 @@ function ConfigBaseInfoController($rootScope, $scope, $window, $location, $trans
 
         });
 
+
         PermissionService.has_assign_user_permission(appId).then(function (result) {
             $scope.hasAssignUserPermission = result.hasPermission;
         }, function (result) {
 
         });
 
-      PermissionService.has_manage_access_key_permission(appId).then(
-          function (result) {
-            $scope.hasCreateClusterPermission = result.hasPermission;
-          }, function (result) {
-
-          });
-
         $scope.showMasterPermissionTips = function () {
             $("#masterNoPermissionDialog").modal('show');
         };

+ 0 - 3
apollo-portal/src/main/resources/static/scripts/services/PermissionService.js

@@ -212,9 +212,6 @@ appService.service('PermissionService', ['$resource', '$q', 'AppUtil', function
         has_assign_user_permission: function (appId) {
             return hasAppPermission(appId, 'AssignRole');
         },
-      has_manage_access_key_permission: function (appId) {
-        return hasAppPermission(appId, 'ManageAccessKey');
-      },
         has_modify_namespace_permission: function (appId, namespaceName) {
             return hasNamespacePermission(appId, namespaceName, 'ModifyNamespace');
         },

+ 5 - 0
apollo-portal/src/main/resources/static/styles/common-style.css

@@ -505,6 +505,10 @@ table th {
     background: url(../img/manage.png) no-repeat;
 }
 
+.list-group-item .icon-accesskey-manage {
+    background: url(../img/secret.png) no-repeat;
+}
+
 .list-group-item .icon-plus-orange {
     background: url(../img/add.png) no-repeat;
 }
@@ -803,6 +807,7 @@ table th {
 
 .project-setting .panel-body .context {
     padding-left: 30px;
+    padding-right: 30px;
 }
 
 .app-search-list .select2-container, .app-search-list .select2-container .select2-selection {