在Kubernetes集群内运行微信机器人:Sidecar容器热更新Cookie的Java实现
·
在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 集群中长期稳定运行,避免因会话过期导致服务中断。
更多推荐
所有评论(0)