Java后端服务在Kubernetes环境中安全注入美团API密钥的Secret管理实践

在“吃喝不愁”App对接美团开放平台时,需使用 client_idclient_secret 获取访问令牌。若将密钥硬编码或通过环境变量明文传递,存在泄露风险。本文基于 Kubernetes Secret + Spring Boot 配置加载机制,实现密钥的安全注入、运行时隔离与最小权限访问。

1. 创建Kubernetes Secret

使用 kubectl 创建包含美团API凭证的Opaque类型Secret:

kubectl create secret generic meituan-api-secret \
  --from-literal=client.id=mt_1234567890abcdef \
  --from-literal=client.secret=9876543210fedcba_secret_key \
  -n bajie-prod

验证Secret内容(Base64解码):

kubectl get secret meituan-api-secret -n bajie-prod -o jsonpath='{.data.client\.id}' | base64 -d

在这里插入图片描述

2. 在Deployment中挂载Secret为Volume

避免通过环境变量暴露(易被 ps 或日志捕获),推荐以文件形式挂载:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: bajie-service
  namespace: bajie-prod
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: registry.baodanbao.com.cn/bajie-service:1.2.0
        volumeMounts:
        - name: meituan-secret-volume
          mountPath: /etc/secrets/meituan
          readOnly: true
      volumes:
      - name: meituan-secret-volume
        secret:
          secretName: meituan-api-secret

挂载后,容器内路径 /etc/secrets/meituan/client.id/etc/secrets/meituan/client.secret 即为明文文件(仅容器内可读)。

3. Java配置类读取Secret文件

package baodanbao.com.cn.meituan.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

@Configuration
public class MeituanApiConfig {

    @Value("${meituan.secret.path:/etc/secrets/meituan}")
    private String secretPath;

    private String clientId;
    private String clientSecret;

    @PostConstruct
    public void loadSecrets() throws IOException {
        this.clientId = readFromFile(secretPath + "/client.id");
        this.clientSecret = readFromFile(secretPath + "/client.secret");
    }

    private String readFromFile(String path) throws IOException {
        byte[] bytes = Files.readAllBytes(Paths.get(path));
        return new String(bytes).trim(); // 去除末尾换行符
    }

    public String getClientId() {
        return clientId;
    }

    public String getClientSecret() {
        return clientSecret;
    }
}

4. 使用配置获取访问令牌

package baodanbao.com.cn.meituan.service;

import baodanbao.com.cn.meituan.config.MeituanApiConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class MeituanTokenService {

    @Autowired
    private MeituanApiConfig apiConfig;

    private final WebClient webClient = WebClient.create("https://openapi.meituan.com");

    public Mono<String> fetchAccessToken() {
        return webClient.post()
                .uri("/auth/token")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData("client_id", apiConfig.getClientId())
                        .with("client_secret", apiConfig.getClientSecret())
                        .with("grant_type", "client_credentials"))
                .retrieve()
                .bodyToMono(TokenResponse.class)
                .map(TokenResponse::getAccessToken);
    }

    static class TokenResponse {
        private String access_token;
        public String getAccessToken() { return access_token; }
    }
}

5. 安全加固措施

  • 文件权限:Kubernetes 自动设置 Secret 文件权限为 0644,可通过 defaultMode: 0400 限制:
    volumes:
    - name: meituan-secret-volume
      secret:
        secretName: meituan-api-secret
        defaultMode: 0400
    
  • Pod安全策略:启用 restricted PSP,禁止容器以 root 运行;
  • RBAC最小权限:ServiceAccount 仅允许读取指定 Secret:
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      namespace: bajie-prod
      name: secret-reader
    rules:
    - apiGroups: [""]
      resources: ["secrets"]
      resourceNames: ["meituan-api-secret"]
      verbs: ["get"]
    

6. 本地开发兼容方案

开发环境不依赖K8S,可通过Spring Profile加载本地配置:

application-dev.yml

meituan:
  client-id: ${MT_CLIENT_ID:dev_dummy_id}
  client-secret: ${MT_CLIENT_SECRET:dev_dummy_secret}

修改配置类支持双模式:

@Value("${meituan.client-id:}")
private String externalClientId;

@Value("${meituan.client-secret:}")
private String externalClientSecret;

@PostConstruct
public void loadSecrets() throws IOException {
    if (isK8sEnv()) {
        this.clientId = readFromFile(secretPath + "/client.id");
        this.clientSecret = readFromFile(secretPath + "/client.secret");
    } else {
        this.clientId = externalClientId;
        this.clientSecret = externalClientSecret;
    }
}

private boolean isK8sEnv() {
    return Files.exists(Paths.get("/var/run/secrets/kubernetes.io")); // K8s特征路径
}

7. 密钥轮换与审计

  • 通过 kubectl replace secret 更新密钥后,需滚动重启Pod(或实现热重载);
  • 启用 Kubernetes Audit Log,记录 Secret 读取行为;
  • 定期轮换美团API密钥,并同步更新Secret。

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐