在Kubernetes集群内运行微信机器人:Sidecar容器热更新Cookie的Java实现

在 Kubernetes 中部署基于 Web 协议的微信机器人时,常面临会话状态(如 Cookie)持久化与热更新难题。由于微信 Web 登录依赖动态 Cookie,且会话有效期有限,若主应用容器重启,将丢失登录态。通过 Sidecar 模式,可将 Cookie 管理职责分离:主容器专注业务逻辑,Sidecar 容器负责监听外部凭证变更并热注入共享存储,主容器实时加载最新 Cookie,实现无缝续期。

架构设计:共享 Volume + 文件监听

  • 主容器(Java 微信机器人)与 Sidecar 容器挂载同一 emptyDir 卷;
  • Sidecar 从 Secret、ConfigMap 或外部 API 获取最新 Cookie,并写入 /shared/cookies.json
  • 主容器通过 WatchService 监听该文件变更,动态刷新 HTTP 客户端的 Cookie 存储。

Sidecar 容器:定时拉取并写入 Cookie

以下为 Sidecar 的简化 Go 实现(非 Java,可忽略包名要求),模拟从 Secret 更新 Cookie:

// sidecar/main.go
package main

import (
	"io/ioutil"
	"log"
	"os"
	"time"
)

func main() {
	ticker := time.NewTicker(30 * time.Second)
	for range ticker.C {
		cookieData, err := ioutil.ReadFile("/etc/wechat-secret/cookies.json")
		if err != nil {
			log.Printf("Failed to read secret: %v", err)
			continue
		}
		err = ioutil.WriteFile("/shared/cookies.json", cookieData, 0644)
		if err != nil {
			log.Printf("Failed to update shared cookie: %v", err)
		} else {
			log.Println("Cookie updated in shared volume")
		}
	}
}

Kubernetes Pod 配置片段:

spec:
  containers:
  - name: wechat-bot
    image: wechat-bot:latest
    volumeMounts:
    - name: shared-cookies
      mountPath: /app/shared
  - name: cookie-sidecar
    image: cookie-sidecar:latest
    volumeMounts:
    - name: shared-cookies
      mountPath: /shared
    - name: wechat-secret
      mountPath: /etc/wechat-secret
  volumes:
  - name: shared-cookies
    emptyDir: {}
  - name: wechat-secret
    secret:
      secretName: wechat-login-cookie

在这里插入图片描述

主容器:Java 实现 Cookie 热加载

主应用使用 java.nio.file.WatchService 监听 /app/shared/cookies.json 变更,并更新 HttpClient 的 Cookie 管理器:

package wlkankan.cn.wechat.cookie;

import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

public class HotReloadableCookieManager {

    private final WatchService watchService;
    private final Path cookiePath;
    private final AtomicReference<CookieStore> currentStore = new AtomicReference<>();
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    public HotReloadableCookieManager(String cookieFilePath) throws IOException {
        this.cookiePath = Paths.get(cookieFilePath);
        this.watchService = FileSystems.getDefault().newWatchService();
        this.cookiePath.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
        loadCookieFromFile(); // 初始加载
        startWatcher();
    }

    private void startWatcher() {
        executor.submit(() -> {
            try {
                WatchKey key;
                while ((key = watchService.take()) != null) {
                    for (WatchEvent<?> event : key.pollEvents()) {
                        if (cookiePath.getFileName().toString().equals(event.context().toString())) {
                            loadCookieFromFile();
                        }
                    }
                    key.reset();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

    private void loadCookieFromFile() {
        try {
            String content = Files.readString(cookiePath);
            CookieStore store = CookieStore.fromJson(content);
            currentStore.set(store);
            System.out.println("Hot-reloaded cookie at " + System.currentTimeMillis());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public CookieStore getCurrentStore() {
        return currentStore.get();
    }
}

CookieStore 数据结构与解析

package wlkankan.cn.wechat.cookie;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CookieStore {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @JsonProperty("cookies")
    private List<CookieEntry> cookies = List.of();

    public Map<String, String> toDomainCookieMap(String domain) {
        Map<String, String> map = new ConcurrentHashMap<>();
        for (CookieEntry entry : cookies) {
            if (domain.equals(entry.getDomain()) || entry.getDomain().endsWith(domain)) {
                map.put(entry.getName(), entry.getValue());
            }
        }
        return map;
    }

    public static CookieStore fromJson(String json) throws IOException {
        return MAPPER.readValue(json, CookieStore.class);
    }

    public static class CookieEntry {
        private String name;
        private String value;
        private String domain;
        private String path;
        private long expiry;

        // getters and setters
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getValue() { return value; }
        public void setValue(String value) { this.value = value; }
        public String getDomain() { return domain; }
        public void setDomain(String domain) { this.domain = domain; }
        public long getExpiry() { return expiry; }
        public void setExpiry(long expiry) { this.expiry = expiry; }
    }
}

集成到 HttpClient 请求链路

在发送微信请求时,从 HotReloadableCookieManager 获取最新 Cookie 并注入 Header:

package wlkankan.cn.wechat.client;

import wlkankan.cn.wechat.cookie.CookieStore;
import wlkankan.cn.wechat.cookie.HotReloadableCookieManager;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class WechatWebClient {

    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private final HotReloadableCookieManager cookieManager;
    private static final String WECHAT_DOMAIN = ".wx.qq.com";

    public WechatWebClient(String cookieFilePath) throws Exception {
        this.cookieManager = new HotReloadableCookieManager(cookieFilePath);
    }

    public HttpResponse<String> sendGet(String url) throws Exception {
        CookieStore store = cookieManager.getCurrentStore();
        if (store == null) {
            throw new IllegalStateException("No valid cookie loaded");
        }

        String cookieHeader = store.toDomainCookieMap(WECHAT_DOMAIN).entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .reduce((a, b) -> a + "; " + b)
                .orElse("");

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Cookie", cookieHeader)
                .timeout(Duration.ofSeconds(15))
                .GET()
                .build();

        return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    }
}

该方案通过 Sidecar 解耦凭证管理,主容器无感知实现 Cookie 热更新,保障微信机器人在 Kubernetes 集群中长期稳定运行,避免因会话过期导致服务中断。

更多推荐