欢迎光临
我们一直在努力

微信个人号朋友圈图片下载:连接池复用与SSL SNI在JDK 17下的适配

微信个人号朋友圈图片下载:连接池复用与SSL SNI在JDK 17下的适配

在自动化抓取微信个人号朋友圈图片的场景中,频繁建立 HTTPS 连接会导致性能瓶颈。同时,微信 CDN(如 mmbiz.qpic.cn)依赖 SNI(Server Name Indication) 扩展进行虚拟主机路由。若客户端未正确设置 SNI,在 JDK 17 默认安全策略下将返回 403 或 TLS 握手失败。本文通过 Apache HttpClient 5 + 自定义 SSL Context + 连接池复用,实现高效、兼容的图片下载方案。

构建支持 SNI 的 SSL Connection Socket Factory

JDK 17 对 TLS 安全性要求更高,需显式启用 SNI:

package wlkankan.cn.wechat.ssl;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;

public class WechatSslHttpClientBuilder {

public static CloseableHttpClient createClient() throws NoSuchAlgorithmException {
SSLContext sslContext = SSLContexts.createDefault();

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, (host, port) -> {
// 关键:为每个目标主机创建携带 SNI 的 SSLEngine
SSLEngine engine = sslContext.createSSLEngine(host.getHostName(), port);
engine.setUseClientMode(true);
// 启用 SNI
SSLParameters params = new SSLParameters();
params.setServerNames(java.util.Collections.singletonList(
new SNIHostName(host.getHostName())
));
engine.setSSLParameters(params);
return engine;
});

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(
sslsf,
null,
null,
TimeValue.ofSeconds(30)
);
connManager.setMaxTotal(100);
connManager.setDefaultMaxPerRoute(20);

SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(10_000)
.build();
connManager.setDefaultSocketConfig(socketConfig);

RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Duration.ofSeconds(5))
.setResponseTimeout(Duration.ofSeconds(15))
.build();

return HttpClients.custom()
.setConnectionManager(connManager)
.setDefaultRequestConfig(requestConfig)
.disableCookieManagement()
.build();
}
}

注意:SNIHostName 需导入 javax.net.ssl.SNIHostName(JDK 8+ 内置),确保 Hostname 与 URL 一致。 在这里插入图片描述

图片下载服务实现

package wlkankan.cn.wechat.service;

import wlkankan.cn.wechat.ssl.WechatSslHttpClientBuilder;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;

public class MomentImageDownloader {

private static final Logger log = LoggerFactory.getLogger(MomentImageDownloader.class);
private final CloseableHttpClient httpClient;

public MomentImageDownloader() throws Exception {
this.httpClient = WechatSslHttpClientBuilder.createClient();
}

public CompletableFuture<Void> download(String imageUrl, Path outputPath) {
return CompletableFuture.runAsync(() -> {
HttpGet request = new HttpGet(imageUrl);
// 必须设置 User-Agent,否则微信 CDN 返回 403
request.setHeader("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)");

try (ClassicHttpResponse response = httpClient.execute(request)) {
if (response.getCode() != 200) {
throw new RuntimeException("HTTP " + response.getCode() + " for " + imageUrl);
}
byte[] data = EntityUtils.toByteArray(response.getEntity());
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile())) {
fos.write(data);
}
log.info("Downloaded: {} -> {}", imageUrl, outputPath);
} catch (IOException e) {
log.error("Failed to download image: " + imageUrl, e);
throw new RuntimeException(e);
}
});
}

public void shutdown() throws IOException {
httpClient.close();
}
}

连接池复用与资源管理

为避免每次下载新建客户端,应全局复用 CloseableHttpClient 实例:

// 在 Spring 配置类中
@Bean(destroyMethod = "close")
public CloseableHttpClient wechatHttpClient() throws Exception {
return WechatSslHttpClientBuilder.createClient();
}

// 或在单例服务中持有
public class GlobalImageService {
private static final MomentImageDownloader DOWNLOADER;
static {
try {
DOWNLOADER = new MomentImageDownloader();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static CompletableFuture<Void> download(String url, Path path) {
return DOWNLOADER.download(url, path);
}
}

典型调用示例

List<String> imageUrls = Arrays.asList(
"https://mmbiz.qpic.cn/sz_mmbiz_jpg/xxxxx/0?wx_fmt=jpeg",
"https://mmbiz.qpic.cn/sz_mmbiz_png/yyyyy/0?wx_fmt=png"
);

List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < imageUrls.size(); i++) {
String url = imageUrls.get(i);
Path outputPath = Path.of("/data/images/moment_" + i + ".jpg");
futures.add(GlobalImageService.download(url, outputPath));
}

// 等待全部完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

该方案通过连接池复用减少 TCP/TLS 握手开销,结合显式 SNI 设置确保 JDK 17 下能正常访问微信 CDN 资源,适用于高并发朋友圈图片批量下载场景。

赞(0)
未经允许不得转载:171主机测评 » 微信个人号朋友圈图片下载:连接池复用与SSL SNI在JDK 17下的适配
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址