为了保持描述简短,我有以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>
</dependency>
我已在
application.yaml
中配置为从 Azure Key Vault 检索 OAuth 客户端所需的客户端密钥,我的 OAuth 客户端如下:
spring:
cloud:
azure:
keyvault:
secret:
property-sources:
- name: key-vault-properties
endpoint: https://my-key-vault.vault.azure.net
refresh-interval: 10s
secret-keys:
- test-client-secret
security:
oauth2:
client:
registration:
my-client:
- client-id: id
client-secret: ${test-client-secret}
authorization-grant-type: client_credentials
client-authentication-method: client_secret_basic
provider:
my-provider:
- token-uri: https://token-uri.com
这一切都很好。但是,出于安全原因,我需要每年轮换客户端密钥。
我尝试在 Key Vault 中添加新版本的密钥。但当我重新启动应用程序时,我只看到它反映在
ClientRegistration
中。
我的问题:是否可以在不重新启动完整应用程序的情况下动态刷新客户端密钥?如果是这样,我怎样才能实现这一目标?
附加信息:
Spring Boot版本:3.3.3
Spring Cloud Azure 版本:5.14.0
Spring 安全版本:6.3.3
我尝试使用下面的示例 Spring boot 应用程序每 10 分钟轮换一次 Keyvault 密钥,而无需重新启动应用程序。
要安排每年的秘密轮换,您可以在 KeyVaultService 类中指定
fixedRate
参数(以毫秒为单位),如下所示。
@Scheduled(fixedRate = 31_536_000_000L) // 31,536,000,000 ms = 1 year
KeyVaultService.java:
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.Base64;
@Service
public class KeyVaultService {
private final SecretClient secretClient;
private final RefreshScope refreshScope;
@Autowired
public KeyVaultService(SecretClient secretClient, RefreshScope refreshScope) {
this.secretClient = secretClient;
this.refreshScope = refreshScope;
}
public String getSecret(String secretName) {
try {
KeyVaultSecret secret = secretClient.getSecret(secretName);
return secret.getValue();
} catch (Exception e) {
return "Failed to retrieve secret: " + e.getMessage();
}
}
@Scheduled(fixedRate = 600_000) // @Scheduled(fixedRate = 31_536_000_000L)
public void rotateClientSecret() {
String secretName = "test-client-secrets";
String newSecretValue = generateSecureSecret();
try {
secretClient.setSecret(new KeyVaultSecret(secretName, newSecretValue));
System.out.println("Successfully rotated the client secret.");
refreshScope.refreshAll();
} catch (Exception e) {
System.err.println("Failed to rotate client secret: " + e.getMessage());
}
}
private String generateSecureSecret() {
SecureRandom secureRandom = new SecureRandom();
byte[] bytes = new byte[24];
secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}
SecretController.java:
import com.example.keyvaultsecretdemo.service.KeyVaultService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/secrets")
public class SecretController {
private final KeyVaultService keyVaultService;
@Autowired
public SecretController(KeyVaultService keyVaultService) {
this.keyVaultService = keyVaultService;
}
@GetMapping("/get/{secretName}")
public String getSecret(@PathVariable String secretName) {
return keyVaultService.getSecret(secretName);
}
}
KeyVaultConfig.java:
import com.azure.identity.ClientSecretCredential;
import com.azure.identity.ClientSecretCredentialBuilder;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KeyVaultConfig {
@Value("${spring.cloud.azure.keyvault.secret.property-sources[0].endpoint}")
private String vaultUrl;
@Value("${spring.cloud.azure.keyvault.secret.property-sources[0].secret-keys[0]}")
private String secretKey;
@Value("${spring.security.oauth2.client.registration.my-client.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.my-client.client-secret}")
private String clientSecret;
@Value("${spring.security.oauth2.client.registration.my-client.tenant-id}")
private String tenantId;
@Bean
public SecretClient secretClient() {
ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
.clientId(clientId)
.clientSecret(clientSecret)
.tenantId(tenantId)
.build();
return new SecretClientBuilder()
.vaultUrl(vaultUrl)
.credential(clientSecretCredential)
.buildClient();
}
}
application.yml:
spring:
cloud:
config:
enabled: false
azure:
keyvault:
secret:
property-sources:
- name: key-vault-properties
endpoint: https://<keyvaultName>.vault.azure.net/
refresh-interval: 10s
secret-keys:
- test-client-secrets
security:
oauth2:
client:
registration:
my-client:
client-id: <clientID>
client-secret: ${test-client-secrets}
tenant-id: <tenantID>
authorization-grant-type: client_credentials
client-authentication-method: client_secret_basic
provider:
my-provider:
token-uri: https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-secrets</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.14.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core</artifactId>
<version>1.53.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.3.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>
<version>5.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
终端输出:
浏览器输出:
我在浏览器中得到了秘密,如下所示。
http://localhost:8080/api/secrets/get/{secretName}
密钥库秘密:
Azure Keyvault 秘密版本每 10 分钟更新一次,如下所示。