本文将介绍如何在不依赖任何第三方库的情况下,使用纯.NET实现企业微信应用的快速接入,涵盖域名验证和消息处理两个核心功能。
1. 背景
作为.NET开发者,在开发企业内部应用的时候,我们经常需要与企业微信进行集成,实现快捷的内部登录和一些便捷的消息交互,简化一些功能的开发和重复建设。虽然市面上有一些第三方库可以帮助我们完成这些集成工作,但有时候我们希望能够直接使用.NET自带的功能来实现这些需求,以减少对外部依赖的依赖,提升代码的可维护性和安全性。
下面我们就开始使用纯.NET实现企业微信应用的快速接入,企业微信应用接入主要包含两个关键环节:
- 域名归属认证:验证域名所有权
- 消息接收处理:处理企业微信推送的事件和消息
2. 项目结构与配置
2.1 配置文件设置
首先在appsettings.json中配置企业微信相关参数:
json
{
"DomainVerification": {
"xxxx": "xxxxxx"
},
"WeCom": {
"Token": "企业微信后台设置的Token",
"EncodingAesKey": "企业微信后台设置的EncodingAesKey",
"CorpId": "企业ID",
"CorpSecret": "应用凭证密钥"
}
}
2.2 配置模型定义
使用 record 类型定义配置模型:
cs
public record WeCom(string CorpId, string CorpSecret, string Token, string EncodingAesKey);
2.3 依赖注入注册
在 Program.cs 中注册配置:
cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<WeCom>(builder.Configuration.GetSection(nameof(WeCom)));
3. 域名归属认证实现
企业微信要求通过特定格式的URL验证域名归属,我们可以使用Minimal API实现:
cs
app.MapGet("/WW_verify_{name}.txt", (string name) =>
{
var cfg = app.Configuration;
var value = cfg.GetValue<string>($"DomainVerification:{name}");
if (string.IsNullOrEmpty(value))
{
return Results.NotFound();
}
return Results.Text(value, "text/plain");
});

实现要点:
- 使用路由模板
WW_verify_{name}.txt匹配企业微信的验证请求 - 从配置中读取对应的验证码内容
- 返回纯文本响应
4. 消息接收处理控制器
4.1 控制器基础结构
cs
[ApiController]
[Route("wecom/callback")]
public class WeComController : ControllerBase
{
private readonly ILogger<WeComController> _logger;
private readonly WeCom _weCom;
public WeComController(ILogger<WeComController> logger, IOptionsSnapshot<WeCom> weCom)
{
_logger = logger;
_weCom = weCom.Value;
}
// 后续方法实现...
}
4.2 URL验证接口(GET请求)
企业微信在配置回调URL时会发送GET请求进行验证:
cs
[HttpGet]
public IActionResult Get([FromQuery] string msg_signature, [FromQuery] string timestamp,
[FromQuery] string nonce, [FromQuery] string echostr)
{
var token = _weCom.Token;
var encodingAesKey = _weCom.EncodingAesKey;
var corpId = _weCom.CorpId;
// 配置验证
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(encodingAesKey) || string.IsNullOrEmpty(corpId))
{
_logger.LogWarning("WeCom config missing");
return BadRequest("WeCom config missing");
}
if (string.IsNullOrEmpty(echostr))
return BadRequest();
// 签名验证
if (!VerifySignature(token, timestamp, nonce, echostr, msg_signature))
{
_logger.LogWarning("WeCom signature invalid (GET)");
return BadRequest("signature invalid");
}
try
{
var plain = DecryptMessage(encodingAesKey, echostr, corpId);
// 返回明文完成验证
return Content(plain, "text/plain", Encoding.UTF8);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "WeCom decrypt failed (GET)");
return BadRequest("decrypt failed");
}
}
4.3 消息处理接口(POST请求)
处理企业微信推送的各种事件和消息:
cs
[HttpPost]
public async Task<IActionResult> Post([FromQuery] string msg_signature,
[FromQuery] string timestamp, [FromQuery] string nonce)
{
var token = _weCom.Token;
var encodingAesKey = _weCom.EncodingAesKey;
var corpId = _weCom.CorpId;
// 配置验证
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(encodingAesKey) || string.IsNullOrEmpty(corpId))
{
_logger.LogWarning("WeCom config missing");
return BadRequest();
}
// 读取请求体
using var reader = new System.IO.StreamReader(Request.Body, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
var encrypt = ExtractXmlNode(body, "Encrypt");
if (string.IsNullOrEmpty(encrypt))
{
// 非加密消息或格式不对,按企业微信要求返回 success
_logger.LogDebug("WeCom POST without Encrypt node, return success");
return Content("success", "text/plain");
}
// 签名验证
if (!VerifySignature(token, timestamp, nonce, encrypt, msg_signature))
{
_logger.LogWarning("WeCom signature invalid (POST)");
return BadRequest("signature invalid");
}
string xml;
try
{
xml = DecryptMessage(encodingAesKey, encrypt, corpId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "WeCom decrypt failed (POST)");
return BadRequest("decrypt failed");
}
// 处理XML消息内容
// 这里可以添加具体的业务逻辑处理
// 企业微信要求返回固定字符串 "success"
return Content("success", "text/plain");
}
5. 核心工具方法实现
5.1 签名验证
cs
private static bool VerifySignature(string token, string timestamp, string nonce,
string encrypt, string signature)
{
var arr = new[] { token ?? string.Empty, timestamp ?? string.Empty,
nonce ?? string.Empty, encrypt ?? string.Empty };
Array.Sort(arr, StringComparer.Ordinal);
var raw = string.Join("", arr);
using var sha1 = SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(raw));
var computed = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
return string.Equals(computed, signature ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
5.2 消息解密
cs
private static string DecryptMessage(string encodingAesKey, string inputEncryptBase64, string corpId)
{
// 补齐 base64 填充
var pad = encodingAesKey;
if (pad.Length % 4 != 0)
pad += new string('=', 4 - pad.Length % 4);
var aesKey = Convert.FromBase64String(pad);
var encrypted = Convert.FromBase64String(inputEncryptBase64);
using var aes = Aes.Create();
aes.Key = aesKey;
aes.IV = aesKey.Take(16).ToArray();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
using var decryptor = aes.CreateDecryptor();
var decrypted = decryptor.TransformFinalBlock(encrypted, 0, encrypted.Length);
// 移除 PKCS#7 填充
var padLen = decrypted[^1];
if (padLen < 1 || padLen > 32) padLen = 0;
var noPad = decrypted[..(decrypted.Length - padLen)];
// 结构:16 random | 4 msgLen | msg | corpId
var msgLenBytes = noPad.Skip(16).Take(4).ToArray();
if (msgLenBytes.Length < 4) throw new Exception("invalid msg len");
// 处理字节序
if (BitConverter.IsLittleEndian)
Array.Reverse(msgLenBytes);
var msgLen = BitConverter.ToInt32(msgLenBytes, 0);
var msgBytes = noPad.Skip(20).Take(msgLen).ToArray();
var msg = Encoding.UTF8.GetString(msgBytes);
var fromCorpIdBytes = noPad.Skip(20 + msgLen).ToArray();
var fromCorpId = Encoding.UTF8.GetString(fromCorpIdBytes);
// 验证企业ID
if (!string.IsNullOrEmpty(corpId) && !string.IsNullOrEmpty(fromCorpId))
{
if (!fromCorpId.StartsWith(corpId, StringComparison.Ordinal))
throw new Exception("corpId mismatch");
}
return msg;
}
5.3 XML节点提取
cs
private static string ExtractXmlNode(string xml, string nodeName)
{
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var node = doc.SelectSingleNode($"/xml/{nodeName}");
return node?.InnerText;
}
catch
{
return null;
}
}
6. 测试与部署
在实际部署和测试时,请确保完成以下步骤:
- 域名验证测试 :访问
https://your-domain.com/WW_verify_xxxx.txt验证是否返回正确内容 - URL配置 :在企业微信后台配置回调URL为
https://your-domain.com/wecom/callback - 消息测试 :通过企业微信的在线测试工具测试集成效果
7. 最后
本文完整展示了如何使用纯.NET技术栈实现企业微信应用的接入,涵盖了从域名验证到消息处理的完整流程。这种实现方式不依赖任何第三方库,代码简洁清晰,易于理解和维护,适合需要快速接入企业微信的.NET项目使用。您可以根据实际业务需求在此基础上进行扩展,如添加消息类型处理、业务逻辑集成等功能。