
简介本资源是一套基于SpringBoot实现的企业微信会话内容存档完整解决方案面向Java后端开发者及企业级IM集成工程师解决企业合规审计场景下会话数据实时拉取、加解密与持久化存储的核心问题。资源包共153个文件含37个核心Java类覆盖SDK封装、消息解密、回调验签、数据库存档等模块、11个典型业务场景Sample示例、4个Linux Docker部署脚本及配置模板另有DLLWindows与SOLinux动态库支持跨平台加解密整体压缩包仅8.77MB轻量易集成。已有2851人学习下载项目严格遵循企业微信官方API文档开发提供从公私钥配置、会话拉取流程到异常重试机制的全链路实现并在README中附作者联系方式便于技术答疑。读者可直接复用其模块化代码结构快速落地会话存档功能同时深入理解企业微信安全通信机制与SpringBoot高并发处理实践。1. SpringBoot 实现企业微信会话内容存档不是调个 API 就完事而是要打通 Windows/Linux 双环境下的证书加载、HTTP 签名、敏感数据落库与服务自启闭环企业微信「会话内容存档」能力常被误认为是“开个开关、填个 URL 就能收消息”的轻量功能。实际落地时90% 的失败案例卡在非业务层Windows 上keystore路径含中文导致FileNotFoundExceptionLinux 下systemd服务因JAVA_HOME未显式声明而启动即退出更隐蔽的是——企业微信要求的SHA256withRSA签名必须用私钥原始字节计算摘要而非 Spring Boot 默认的RestTemplate或WebClient自动签名逻辑。本方案不依赖任何第三方 SDK 封装全程基于 Spring Boot 原生能力构建可审计、可灰度、可双平台部署的存档服务。适合已通过企业微信「会话内容存档」资质审核、需自主掌控数据主权的中大型企业 IT 团队尤其关注国产 Linux如麒麟、统信兼容性与 Windows Server 生产环境稳定性。2. 从企业微信控制台到 Spring Boot 工程完成资质配置、密钥生成与基础项目结构搭建2.1 企业微信侧必须完成的 4 项前置配置缺一不可企业微信管理后台的「应用管理 → 会话内容存档」模块中以下操作必须由超级管理员完成且顺序不可颠倒开通资质并绑定主体提交《会话内容存档服务开通申请表》并完成企业认证确保「服务状态」显示为“已开通”配置回调 URL 与 Token/EncodingAESKey填写你 Spring Boot 服务的公网可访问地址如https://api.yourcompany.com/wxwork/archive/callbackToken 和 EncodingAESKey 需自行生成并记录建议用openssl rand -base64 32生成下载并保管好「企业证书」与「CA 根证书」点击「下载证书」获取corpid_cert.p12含私钥和ca.crt根证书二者将用于服务端 HTTPS 客户端身份认证与响应验签授权存档范围在「存档范围设置」中明确选择需存档的部门、成员或外部联系人类型注意未在此处勾选的成员其会话内容即使触发回调也不会被推送。提示corpid_cert.p12是带密码的 PKCS#12 文件密码在下载时由企业微信生成并显示一次请立即保存。该密码后续将作为 Spring Boot 配置项wxwork.cert.password使用切勿硬编码在代码中。2.2 初始化 Spring Boot 项目并集成核心依赖使用 Spring Initializr推荐spring-boot-starter-parent 3.2.7创建基础工程关键依赖如下pom.xml片段dependencies !-- Web 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 配置中心支持为后续多环境部署铺垫 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency !-- JSON 处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- 数据库以 MySQL 8.0 为例 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- 日志增强便于排查跨平台路径问题 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /dependency /dependencies项目结构需包含以下关键包config/存放WxWorkArchiveConfig.java封装所有企业微信配置service/ArchiveCallbackService.java处理回调、ArchiveDownloadService.java下载加密消息体util/WxWorkSignatureUtil.java实现 SHA256withRSA 签名与验签、PfxKeyStoreUtil.java安全加载 P12 证书entity/ArchiveMessage.java映射解密后的会话消息实体2.3 构建可跨平台加载的证书资源路径策略Windows 与 Linux 对文件路径的解析差异是部署失败主因。Spring Boot 默认classpath:无法定位外部证书而绝对路径又破坏可移植性。正确做法是将证书文件置于application.yml同级目录并通过--spring.config.locationfile:./config/指定配置位置再用ResourceLoader动态加载。在application.yml中定义wxwork: # 企业 ID必填 corpid: wwxxxxxxxxxxxxxxxx # 回调 Token必填 token: your_callback_token_here # 消息加解密 Key必填 encoding-aes-key: your_encoding_aes_key_here # 证书文件路径相对 application.yml 所在目录 cert-path: ./certs/corpid_cert.p12 # 证书密码从环境变量读取禁止明文 cert-password: ${WXWORK_CERT_PASSWORD:changeit} # CA 根证书路径 ca-cert-path: ./certs/ca.crt对应 Java 配置类WxWorkArchiveConfig.java中使用ResourceLoader安全读取Component ConfigurationProperties(prefix wxwork) Data public class WxWorkArchiveConfig { private String corpid; private String token; private String encodingAesKey; private String certPath; private String certPassword; private String caCertPath; Autowired private ResourceLoader resourceLoader; /** * 获取 P12 证书 Resource 对象自动适配 Windows/Linux 路径分隔符 */ public Resource getCertResource() throws IOException { // 尝试 classpath 加载开发测试用 Resource classpathRes resourceLoader.getResource(classpath: certPath); if (classpathRes.exists()) { return classpathRes; } // 尝试文件系统加载生产部署用 Resource fileRes resourceLoader.getResource(file: certPath); if (fileRes.exists()) { return fileRes; } throw new FileNotFoundException(Certificate file not found at: certPath); } /** * 获取 CA 根证书 Resource 对象 */ public Resource getCaCertResource() throws IOException { Resource classpathRes resourceLoader.getResource(classpath: caCertPath); if (classpathRes.exists()) { return classpathRes; } Resource fileRes resourceLoader.getResource(file: caCertPath); if (fileRes.exists()) { return fileRes; } throw new FileNotFoundException(CA certificate file not found at: caCertPath); } }注意ResourceLoader的getResource(file:...)在 Windows 下自动处理\与/在 Linux 下直接使用/无需条件判断。此设计使cert-path配置值在 Windows 和 Linux 下完全一致如均写./certs/corpid_cert.p12彻底规避路径拼接错误。3. 实现企业微信回调接收与消息解密绕过 SDK 封装直击 HTTP 签名验证与 AES-256-GCM 解密核心逻辑3.1 编写符合企业微信规范的回调 Controller企业微信回调请求为POSTContent-Type: application/json且必须返回200 OK且响应体为空字符串否则视为失败并重试。Spring Boot Controller 必须禁用默认 JSON 序列化直接操作InputStreamRestController RequestMapping(/wxwork/archive/callback) Slf4j public class ArchiveCallbackController { Autowired private ArchiveCallbackService archiveCallbackService; PostMapping(consumes MediaType.APPLICATION_JSON_VALUE) public ResponseEntityVoid handleCallback( HttpServletRequest request, RequestBody byte[] rawBody) { try { // 1. 从 Header 提取必需参数 String msgSignature request.getHeader(msg_signature); String timestamp request.getHeader(timestamp); String nonce request.getHeader(nonce); // 2. 验证签名关键必须用原始字节不能先转 String 再 UTF-8 boolean isValid WxWorkSignatureUtil.verifyCallbackSignature( msgSignature, timestamp, nonce, rawBody, archiveCallbackService.getConfig().getToken(), archiveCallbackService.getConfig().getEncodingAesKey() ); if (!isValid) { log.warn(Invalid callback signature. timestamp{}, nonce{}, timestamp, nonce); return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } // 3. 解密消息体 String decryptedXml archiveCallbackService.decryptMessage(rawBody); // 4. 解析 XML 并持久化见 3.3 节 archiveCallbackService.processDecryptedXml(decryptedXml); // 5. 返回空响应体 200 return ResponseEntity.ok().build(); } catch (Exception e) { log.error(Error handling callback, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } }提示RequestBody byte[] rawBody是强制要求。若用RequestBody String bodySpring MVC 会先按UTF-8解码导致 AES 解密时字节流错乱。企业微信文档明确要求“使用原始 POST Body 字节”。3.2 手动实现 SHA256withRSA 签名验证企业微信专用算法企业微信回调签名规则为对token timestamp nonce body的 UTF-8 字节进行 SHA256 摘要再用企业微信提供的公钥从ca.crt提取进行 RSA 验证。WxWorkSignatureUtil.java核心逻辑如下public class WxWorkSignatureUtil { /** * 验证回调签名 * param msgSignature header 中的 msg_signature * param timestamp header 中的 timestamp * param nonce header 中的 nonce * param bodyBytes 原始 POST Body 字节未解码 * param token 企业微信后台配置的 Token * param encodingAesKey 消息加解密 Key * return true if valid */ public static boolean verifyCallbackSignature( String msgSignature, String timestamp, String nonce, byte[] bodyBytes, String token, String encodingAesKey) { try { // 步骤1构造待签名字符串注意顺序与拼接方式 String rawString token timestamp nonce new String(bodyBytes, StandardCharsets.UTF_8); byte[] rawBytes rawString.getBytes(StandardCharsets.UTF_8); // 步骤2计算 SHA256 摘要 MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] sha256Bytes digest.digest(rawBytes); // 步骤3Base64 解码签名 byte[] signatureBytes Base64.getDecoder().decode(msgSignature); // 步骤4从 CA 证书提取公钥此处简化实际应从 WxWorkArchiveConfig 注入 PublicKey publicKey loadPublicKeyFromCaCert(); // 实现见下文 // 步骤5RSA 验证 Signature signature Signature.getInstance(SHA256withRSA); signature.initVerify(publicKey); signature.update(sha256Bytes); return signature.verify(signatureBytes); } catch (Exception e) { log.error(Signature verification failed, e); return false; } } private static PublicKey loadPublicKeyFromCaCert() throws Exception { // 从 WxWorkArchiveConfig 获取 caCertResource读取 X.509 证书 // 此处为示意真实代码需注入 Config CertificateFactory cf CertificateFactory.getInstance(X.509); InputStream is new FileInputStream(./certs/ca.crt); // 生产环境应从 Resource 加载 X509Certificate cert (X509Certificate) cf.generateCertificate(is); return cert.getPublicKey(); } }3.3 解密 AES-256-GCM 加密的消息体并解析 XML企业微信回调的body是 AES-256-GCM 加密的 XML密钥为encodingAesKey的 Base64 解码结果IV 为前 12 字节。解密后需解析xml结构提取Encrypt字段再二次解密得到最终消息Service Slf4j public class ArchiveCallbackService { Autowired private WxWorkArchiveConfig config; public String decryptMessage(byte[] encryptedBody) throws Exception { // 1. 解析 JSON提取 encrypt 字段企业微信回调是 JSON 包裹 XML ObjectMapper mapper new ObjectMapper(); JsonNode rootNode mapper.readTree(encryptedBody); String encryptStr rootNode.path(encrypt).asText(); // 2. Base64 解码 encrypt 字段 byte[] encryptedBytes Base64.getDecoder().decode(encryptStr); // 3. 提取 IV前 12 字节和密文剩余部分 byte[] iv new byte[12]; System.arraycopy(encryptedBytes, 0, iv, 0, 12); byte[] cipherText new byte[encryptedBytes.length - 12]; System.arraycopy(encryptedBytes, 12, cipherText, 0, cipherText.length); // 4. 构造 AES 密钥encodingAesKey Base64 解码后取前 32 字节 byte[] aesKeyBytes Base64.getDecoder().decode(config.getEncodingAesKey()); SecretKeySpec keySpec new SecretKeySpec(aesKeyBytes, 0, 32, AES); // 5. GCM 解密 Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec spec new GCMParameterSpec(128, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, spec); byte[] plainBytes cipher.doFinal(cipherText); // 6. 去除 PKCS#7 填充企业微信使用此填充 int padLen plainBytes[plainBytes.length - 1] 0xFF; byte[] xmlBytes new byte[plainBytes.length - padLen]; System.arraycopy(plainBytes, 0, xmlBytes, 0, xmlBytes.length); return new String(xmlBytes, StandardCharsets.UTF_8); } public void processDecryptedXml(String xmlContent) { // 使用 JAXB 或 Dom4j 解析 xml提取 MsgId, FromUserName, ToUserName, CreateTime, Content 等字段 // 存入数据库 archive_message 表表结构见 4.1 节 // 此处省略具体解析代码重点在于Content 字段可能含敏感信息需脱敏存储 } }注意encodingAesKey是 43 位 Base64 字符串解码后为 32 字节恰好是 AES-256 密钥长度。GCM 模式 IV 长度固定为 12 字节企业微信文档明确指定不可更改。4. 构建 Windows 与 Linux 双平台可部署包从 JAR 打包、证书放置到 systemd / Windows Service 全流程4.1 目录结构与生产部署包组织统一标准无论 Windows 还是 Linux生产部署包必须采用同一目录结构确保配置一致性archive-service/ ├── app.jar # Spring Boot 打包的 fat jar ├── application-prod.yml # 生产环境配置覆盖通用配置 ├── certs/ # 证书目录必须存在 │ ├── corpid_cert.p12 # 企业证书 │ └── ca.crt # CA 根证书 ├── logs/ # 日志目录Linux 下需 chmod 755 └── start.sh / start.bat # 启动脚本平台专属application-prod.yml示例强调server.port与spring.profiles.activespring: profiles: active: prod main: allow-bean-definition-overriding: true server: port: 8080 shutdown: graceful # 关键指定配置文件加载位置使 certs/ 目录可被 ResourceLoader 定位 spring: config: location: file:./ import: optional:file:./application-prod.yml # 数据库连接Linux 下推荐使用 socket 连接提升性能 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/archive_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: archive_user password: ${DB_PASSWORD:changeme} driver-class-name: com.mysql.cj.jdbc.Driver # 日志输出到文件 logging: file: name: ./logs/archive-service.log level: root: INFO com.yourcompany.archive: DEBUG4.2 Linux 系统部署systemd 服务配置与权限加固在 Linux含麒麟、统信等国产系统上必须使用systemd管理服务禁止直接nohup java -jar启动。创建/etc/systemd/system/archive-service.service[Unit] DescriptionEnterprise WeChat Archive Service Afternetwork.target [Service] Typesimple Userarchiveuser Grouparchiveuser WorkingDirectory/opt/archive-service ExecStart/usr/bin/java -Djava.security.egdfile:/dev/./urandom -Xms512m -Xmx1024m -jar /opt/archive-service/app.jar --spring.config.locationfile:/opt/archive-service/ --spring.profiles.activeprod Restartalways RestartSec10 # 关键显式声明 JAVA_HOME避免 systemd 环境变量缺失 EnvironmentJAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64 # 限制资源防止 OOM MemoryLimit1.5G CPUQuota200% [Install] WantedBymulti-user.target执行部署命令以 Ubuntu/Debian 为例# 1. 创建用户与目录 sudo useradd -r -s /bin/false archiveuser sudo mkdir -p /opt/archive-service/certs /opt/archive-service/logs sudo chown -R archiveuser:archiveuser /opt/archive-service # 2. 复制文件假设当前目录为 archive-service/ sudo cp app.jar /opt/archive-service/ sudo cp application-prod.yml /opt/archive-service/ sudo cp -r certs/ /opt/archive-service/ sudo cp start.sh /opt/archive-service/ # 3. 启用并启动服务 sudo systemctl daemon-reload sudo systemctl enable archive-service.service sudo systemctl start archive-service.service # 4. 查看日志验证证书加载 sudo journalctl -u archive-service.service -f # 正常应看到Loaded keystore from file:/opt/archive-service/certs/corpid_cert.p12提示JAVA_HOME必须在Environment中显式声明。systemd 服务默认不继承 shell 的环境变量which java在服务内无效会导致java: command not found错误。4.3 Windows Server 部署使用 winsw 将 JAR 注册为 Windows ServiceWindows Server 不推荐使用sc create因其不支持 JVM 参数与日志重定向。winswWindows Service Wrapper是微软官方推荐方案支持.NET Core与 Java 服务。下载winsw-x64.exe GitHub Releases 重命名为archive-service.exe创建同名配置文件archive-service.xmlservice idarchive-service/id nameEnterprise WeChat Archive Service/name descriptionStores and manages WeCom chat records/description executablejava/executable arguments-Xms512m -Xmx1024m -Dfile.encodingUTF-8 -jar app.jar --spring.config.locationfile:./ --spring.profiles.activeprod/arguments logmoderotate/logmode onfailure actionrestart delay10 sec/ startmodeAutomatic/startmode environment JAVA_HOMEC:\Program Files\Java\jdk-17/JAVA_HOME /environment /service将archive-service.exe,archive-service.xml,app.jar,application-prod.yml,certs/放入同一目录如C:\archive-service以管理员身份运行 CMDcd C:\archive-service archive-service.exe install archive-service.exe start注意JAVA_HOME路径中的空格必须用引号包裹XML 中已处理。logmoderotate会自动生成archive-service.wrapper.log和archive-service.log便于排查FileNotFoundException类错误。5. 数据库建表与敏感信息防护MySQL 表结构设计、字段脱敏与国产 Linux 下的字符集适配5.1 MySQL 8.0 建表语句适配麒麟/统信等国产系统企业微信会话内容含大量中文、Emoji 及特殊符号必须使用utf8mb4字符集与utf8mb4_0900_as_cs排序规则MySQL 8.0 推荐CREATE DATABASE IF NOT EXISTS archive_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; USE archive_db; CREATE TABLE archive_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, msg_id VARCHAR(64) NOT NULL COMMENT 消息唯一ID企业微信生成, from_user_id VARCHAR(64) NOT NULL COMMENT 发送者UserID, to_user_id VARCHAR(64) NOT NULL COMMENT 接收者UserID可能是部门ID或外部联系人ID, create_time BIGINT NOT NULL COMMENT 消息创建时间戳毫秒, msg_type VARCHAR(32) NOT NULL COMMENT 消息类型text/image/video/voice/location/link..., content TEXT COMMENT 文本消息内容已脱敏, media_id VARCHAR(128) COMMENT 媒体文件ID图片/语音/视频, file_name VARCHAR(255) COMMENT 文件名仅当 msg_typefile 时, is_external TINYINT(1) DEFAULT 0 COMMENT 是否为外部联系人0-内部1-外部, archived_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 存档时间, INDEX idx_msg_id (msg_id), INDEX idx_from_user (from_user_id), INDEX idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_as_cs COMMENT企业微信会话内容存档主表;提示utf8mb4_0900_as_cs是大小写敏感、口音敏感的排序规则避免WeCom与wecom被视为相同符合企业微信 UserID 的严格匹配要求。在麒麟系统上若 MySQL 未启用此排序规则需在my.cnf中添加collation-server utf8mb4_0900_as_cs。5.2 敏感信息脱敏策略符合《个人信息保护法》要求content字段不得明文存储手机号、身份证号、银行卡号。在ArchiveCallbackService.processDecryptedXml()中插入脱敏逻辑public class SensitiveDataMasker { // 手机号138****1234 public static String maskMobile(String text) { return text.replaceAll((1[3-9]\\d{4})\\d{4}(\\d{4}), $1****$2); } // 身份证号110101********1234 public static String maskIdCard(String text) { return text.replaceAll((\\d{4})(\\d{10})(\\d{4}), $1********$3); } // 银行卡号6228**********1234保留前6后4 public static String maskBankCard(String text) { return text.replaceAll((\\d{6})\\d{12}(\\d{4}), $1******$2); } public static String maskAll(String text) { if (text null) return null; return maskBankCard(maskIdCard(maskMobile(text))); } } // 在 processDecryptedXml 中调用 String maskedContent SensitiveDataMasker.maskAll(originalContent); archiveMessage.setContent(maskedContent);5.3 验证双平台部署成功的关键检查点部署完成后必须逐项验证以下 5 个检查点任一失败即表示环境未就绪检查项Windows 验证命令Linux 验证命令预期结果1. 证书加载Get-Content C:\archive-service\logs\archive-service.wrapper.log | Select-String Loaded keystorejournalctl -u archive-service.service | grep Loaded keystore输出Loaded keystore from file:C:\archive-service\certs\corpid_cert.p12或类似路径2. 端口监听netstat -ano | findstr :8080sudo ss -tuln | grep :8080显示LISTEN状态PID 对应java进程3. 数据库连接在archive-service目录下执行java -cp app.jar com.yourcompany.archive.util.DbTest简易测试类同上或curl -X GET http://localhost:8080/actuator/health返回{status:UP}且日志无Connection refused4. 回调签名验证查看archive-service.wrapper.log最近 10 行tail -10 /opt/archive-service/logs/archive-service.log出现Valid callback signature日志无Invalid callback signature5. 消息入库SELECT COUNT(*) FROM archive_db.archive_message;通过 MySQL 客户端同上数值 0且create_time与当前时间偏差 5 分钟提示第 5 项需在企业微信后台「会话内容存档」页面手动触发一次「测试回调」或让测试成员发送一条文本消息。这是唯一能验证端到端链路的手段。本文还有配套的精品资源点击获取