通过KMS托管Redis密码凭证

当您的应用需要访问云数据库 Redis 版时,您可以将Redis账号口令存储在KMS的凭据中(即Redis凭据),业务应用通过集成阿里云SDK、KMS SDK或凭据SDK向KMS动态获取账号口令。您还可以为凭据配置轮转,以减少账号口令的泄露风险。

功能介绍

在KMS托管Redis账号口令时,应用程序将无需配置静态数据库账号口令。管理员在KMS创建Redis凭据,应用程序调用GetSecretValue接口获取Redis数据库账号和口令信息,用于访问Redis数据库。

例如您在KMS中自定义的Redis实例凭证为username时,KMS最终会在Redis实例中创建usernameusername_clone账号,实现双账号托管,并使用该账号访问Redis实例,相比较单账号,双账号托管场景下应用程序的可用性、安全性更高。您可以在KMS控制台设置账号轮转策略,默认情况下KMS每24小时会进行账号轮转,即使用不同的账号登录Redis实例,提高安全性。更多信息请参见Redis凭据

重要

请勿在Redis中修改或删除账号口令,以避免业务失败。

image

使用限制

  • 不支持在Redis控制台修改KMS创建的托管型账号的密码,您可以前往KMS控制台通过手动轮转或配置自动轮转策略更换该密码,更多信息请参见轮转Redis凭据

  • 不支持在Redis控制台删除KMS创建的托管型账号,如需删除,请前往KMS控制台执行删除操作,更多信息请参见删除Redis凭据

  • 不支持在Redis控制台修改KMS创建的托管型账号的备注信息。

前提条件

  • 已创建ECS实例,用于连接Redis实例。本示例ECS的操作系统为Alibaba Cloud Linux 3.2104 LTS 64位,同时已安装JAVA 1.8.0。

  • 若使用RAM用户(子账号)或RAM角色管理Redis凭据,您需要为该角色授予系统权限策略AliyunKMSSecretAdminAccess。具体操作请参见授权权限

操作步骤

  1. 创建并启用KMS。具体操作请参见创建和启用KMS

    创建KMS时需选择VPC,请选择与ECS相同的VPC网络。

    若您已创建KMS,请在KMS中添加ECS的VPC网络,具体操作请参见配置VPC

  2. 创建应用接入点。具体操作请参见创建应用接入点

    创建后,浏览量会自动下载ClientKey信息,包含应用身份凭证内容(ClientKeyContent,JSON文件)凭证口令(ClientKeyPassword),请妥善保存。

  3. 下载KMS的CA证书,您可以在KMS控制台的实例管理页面进行下载,更多信息请参见获取KMS的CA证书

  4. 创建一个用户主密钥。具体操作请参见密钥管理快速入门

  5. 创建Redis凭据。具体操作请参见创建Redis凭据

  6. 编写Java测试代码。

    1. 在项目中添加Maven依赖,从Maven仓库中自动下载Java安装包。同时还可以将项目依赖打进作业JAR包,需增加下述<build>。

          <dependencies>
              <dependency>
                  <groupId>redis.clients</groupId>
                  <artifactId>jedis</artifactId>
                  <version>5.1.0</version>
              </dependency>
              <dependency>
                  <groupId>com.aliyun</groupId>
                  <artifactId>alibabacloud-dkms-gcs-sdk</artifactId>
                  <version>0.5.2</version>
              </dependency>
              <dependency>
                  <groupId>com.aliyun</groupId>
                  <artifactId>tea</artifactId>
                  <version>1.2.3</version>
              </dependency>
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>1.7.10</version>
              </dependency>
              <dependency>
                  <groupId>ch.qos.logback</groupId>
                  <artifactId>logback-classic</artifactId>
                  <version>1.2.9</version>
              </dependency>
          </dependencies>
      
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-assembly-plugin</artifactId>
                      <version>3.3.0</version>
                      <configuration>
                          <archive>
                              <manifest>
                                  <mainClass>
                                      com.aliyun.KMSJedisTest
                                  </mainClass>
                              </manifest>
                          </archive>
                          <descriptorRefs>
                              <descriptorRef>jar-with-dependencies</descriptorRef>
                          </descriptorRefs>
                      </configuration>
                      <executions>
                          <execution>
                              <id>assemble-all</id>
                              <phase>package</phase>
                              <goals>
                                  <goal>single</goal>
                              </goals>
                          </execution>
                      </executions>
                  </plugin>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <configuration>
                          <source>1.8</source>
                          <target>1.8</target>
                      </configuration>
                  </plugin>
              </plugins>
          </build>
    2. 编写主代码KMSJedisTest.java

      说明

      为了防止每次新建连接都访问KMS获取密码,本示例中增加了一个缓存credentialCacheTime(默认为600s),在缓存周期内,会返回缓存的密码值,而不去访问 KMS,您可以通过setCredentialCacheTime接口来调整缓存的时间,建议不要低于10分钟。

      package com.aliyun;
      
      import java.time.Duration;
      
      import redis.clients.jedis.DefaultJedisClientConfig;
      import redis.clients.jedis.HostAndPort;
      import redis.clients.jedis.Jedis;
      import redis.clients.jedis.JedisPool;
      
      public class KMSJedisTest {
          public static void main(String[] args) throws Exception {
              if (args.length < 2) {
                  System.out.println(
                      "Please input kmsEndpoint, clientKeyFilePath, clientKeyPass, caCertPath, secretName, redisHost");
                  return;
              }
      
              String endpoint = args[0];
              String clientKeyFilePath = args[1];
              String clientKeyPass = args[2];
              String caCertPath = args[3];
              String secretName = args[4];
              KMSRedisCredentialsProvider kmsRedisCredentialsProvider = new KMSRedisCredentialsProvider(endpoint,
                  clientKeyFilePath, clientKeyPass, caCertPath, secretName);
              kmsRedisCredentialsProvider.setCredentialCacheTime(Duration.ofSeconds(10)); // 设置缓存时间,防止频繁请求KMS
      
              String redisHost = args[5];
              JedisPool jedisPool = new JedisPool(HostAndPort.from(redisHost),
                  DefaultJedisClientConfig.builder().credentialsProvider(kmsRedisCredentialsProvider).build());
      
              for (int i = 0; i < Integer.MAX_VALUE; i++) {
                  Thread.sleep(1000);
                  try (Jedis jedis = jedisPool.getResource()) {
                      System.out.println(jedis.set("" + i, "" + i));
                      System.out.println(jedis.get("" + i));
                  } catch (Exception e) {
                      System.out.println(e);
                  }
              }
          }
      }
      
    3. 编写KMSRedisCredentialsProvider.java

      package com.aliyun;
      
      import java.time.Duration;
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;
      
      import org.json.JSONObject;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import redis.clients.jedis.DefaultRedisCredentials;
      import redis.clients.jedis.RedisCredentials;
      import redis.clients.jedis.RedisCredentialsProvider;
      
      import com.aliyun.dkms.gcs.openapi.models.Config;
      import com.aliyun.dkms.gcs.sdk.Client;
      import com.aliyun.dkms.gcs.sdk.models.*;
      
      public class KMSRedisCredentialsProvider implements RedisCredentialsProvider {
          private static final Logger logger = LoggerFactory.getLogger(KMSRedisCredentialsProvider.class);
      
          private final String endpoint;
          private final String clientKeyFilePath;
          private final String clientKeyPass;
          private final String caCertPath;
          private final String secretName;
          private static Client client = null;
      
          // credential cache time
          private Duration credentialCacheTime = Duration.ofSeconds(600);
          private DefaultRedisCredentials cachedCredentials = null;
          private LocalDateTime credentialsExpiration = null;
      
          public KMSRedisCredentialsProvider(String endpoint, String clientKeyFilePath, String clientKeyPass,
              String caCertPath, String secretName) {
              this.endpoint = endpoint;
              this.clientKeyFilePath = clientKeyFilePath;
              this.clientKeyPass = clientKeyPass;
              this.caCertPath = caCertPath;
              this.secretName = secretName;
              createClientInstance(endpoint, clientKeyFilePath, clientKeyPass, caCertPath);
          }
      
          public void setCredentialCacheTime(Duration credentialCacheTime) {
              this.credentialCacheTime = credentialCacheTime;
          }
      
          private static synchronized void createClientInstance(String endpoint, String clientKeyFilePath,
              String clientKeyPass, String caCertPath) {
              if (client == null) {
                  try {
                      client = new Client(new Config()
                          .setProtocol("https")
                          .setEndpoint(endpoint)
                          .setCaFilePath(caCertPath)
                          .setClientKeyFile(clientKeyFilePath)
                          .setPassword(clientKeyPass));
                  } catch (Exception e) {
                      logger.error("Init kms client failed", e);
                      throw new RuntimeException(e);
                  }
              }
          }
      
          @Override
          public RedisCredentials get() {
              try {
                  LocalDateTime now = LocalDateTime.now();
                  // Check cache
                  if (cachedCredentials != null && now.isBefore(credentialsExpiration)) {
                      return cachedCredentials;
                  }
      
                  GetSecretValueRequest request = new GetSecretValueRequest().setSecretName(secretName);
                  GetSecretValueResponse getSecretValueResponse = client.getSecretValue(request);
                  logger.debug("Now: " + now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) +
                      ", getSecretValueRequest: " + request);
                  String secretData = getSecretValueResponse.getSecretData();
                  JSONObject secretObject = new JSONObject(secretData);
                  if (secretObject.get("AccountName") == null || secretObject.get("AccountPassword") == null) {
                      throw new IllegalArgumentException("secretData must contain AccountName and AccountPassword");
                  }
                  cachedCredentials = new DefaultRedisCredentials(secretObject.get("AccountName").toString(),
                      secretObject.get("AccountPassword").toString());
                  credentialsExpiration = now.plusSeconds(credentialCacheTime.getSeconds());
                  return cachedCredentials;
              } catch (Exception e) {
                  logger.error("get secret failed", e);
                  throw new RuntimeException(e);
              }
          }
      
          @Override
          public void prepare() {
              // do nothing
          }
      
          @Override
          public void cleanUp() {
              // do nothing
          }
      }
      
    4. 将整个项目打进JAR包,命令为mvn package

  7. 在ECS中,通过Java SDK连接Redis实例。

    本示例的语法如下:

    java -jar <kms-redis-jar-with-dependencies.jar> <kmsEndpoint> <clientKeyFilePath> <clientKeyPass> <caCertPath> <secretName> <redisHost>

    参数说明:

    • kms-redis-jar-with-dependencies.jar:JAR包,请使用后缀为jar-with-dependencies的JAR包。

    • kmsEndpoint:KMS的VPC地址,您可以在KMS实例详情页获取。

    • clientKeyFilePath:接入点应用身份凭证内容,为步骤2下载的JSON文件。

    • clientKeyPass:接入点凭证口令,内容在步骤2下载的TXT文件中。

    • caCertPath:KMS实例的CA证书,为步骤3下载的PEM文件。

    • secretName:步骤5中创建Redis凭据的名称。

    • redisHostRedis实例的VPC连接地址与端口号,例如r-bp1g727yrai5yh****.redis.rds.aliyuncs.com:6379

    示例:

    java -jar kms-redis-samples-1.0-SNAPSHOT-jar-with-dependencies.jar kst-hzz6674e7fbw21x9x****.cryptoservice.kms.aliyuncs.com /root/clientKey_KAAP.6432ddc6-f23a-4d78-ac84-****4598206b.json 267d1****1cda4415058e1d72ec49e0a /root/PrivateKmsCA_kst-hzz6674e7fbw21x9x****.pem kms-redis r-bp1g727yrai5yh****.redis.rds.aliyuncs.com:6379

    预期返回如下,表示已成功连接:

    0
    OK
    1
    OK
    2
    OK
    3
    OK
    4
    OK
  8. 您可以在KMS控制台执行立即轮转凭据测试,具体操作请参见轮转Redis凭据

    轮转表示KMS将使用另一个账号(usernameusername_clone)访问Redis实例。

    此时,ECS的连接若仍正常,则表示当前可以实现Redis密码滚动功能。

    ...
    30
    OK
    31
    OK
    32
    OK
    33
    OK
  9. Redis实例执行主从切换(HA)测试,观察客户端情况。

    预期返回如下,表示在HA时连接闪断,KMS实例更新凭据并成功重新连接:

    138
    OK
    139
    redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    OK
    142
    OK
    143
    OK

相关文档