首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >使用.NET实现企业微信应用接入:域名验证与消息处理

使用.NET实现企业微信应用接入:域名验证与消息处理

作者头像
桑榆肖物
发布2026-04-14 16:44:19
发布2026-04-14 16:44:19
240
举报

本文将介绍如何在不依赖任何第三方库的情况下,使用纯.NET实现企业微信应用的快速接入,涵盖域名验证和消息处理两个核心功能。

1. 背景

作为.NET开发者,在开发企业内部应用的时候,我们经常需要与企业微信进行集成,实现快捷的内部登录和一些便捷的消息交互,简化一些功能的开发和重复建设。虽然市面上有一些第三方库可以帮助我们完成这些集成工作,但有时候我们希望能够直接使用.NET自带的功能来实现这些需求,以减少对外部依赖的依赖,提升代码的可维护性和安全性。

下面我们就开始使用纯.NET实现企业微信应用的快速接入,企业微信应用接入主要包含两个关键环节:

域名归属认证:验证域名所有权•消息接收处理:处理企业微信推送的事件和消息

2. 项目结构与配置

2.1 配置文件设置

首先在appsettings.json中配置企业微信相关参数:

代码语言:javascript
复制
{
"DomainVerification":{
"xxxx":"xxxxxx"
},
"WeCom":{
"Token":"企业微信后台设置的Token",
"EncodingAesKey":"企业微信后台设置的EncodingAesKey",
"CorpId":"企业ID",
"CorpSecret":"应用凭证密钥"
}
}

2.2 配置模型定义

使用 record 类型定义配置模型:

代码语言:javascript
复制
public record WeCom(stringCorpId,stringCorpSecret,stringToken,stringEncodingAesKey);

2.3 依赖注入注册

Program.cs 中注册配置:

代码语言:javascript
复制
var builder =WebApplication.CreateBuilder(args);
builder.Services.Configure<WeCom>(builder.Configuration.GetSection(nameof(WeCom)));

3. 域名归属认证实现

企业微信要求通过特定格式的URL验证域名归属,我们可以使用Minimal API实现:

代码语言:javascript
复制
app.MapGet("/WW_verify_{name}.txt",(string name)=>
{
var cfg = app.Configuration;
var value = cfg.GetValue<string>($"DomainVerification:{name}");
if(string.IsNullOrEmpty(value))
{
returnResults.NotFound();
}

returnResults.Text(value,"text/plain");
});

可信域名

实现要点

•使用路由模板WW_verify_{name}.txt匹配企业微信的验证请求•从配置中读取对应的验证码内容•返回纯文本响应

4. 消息接收处理控制器

4.1 控制器基础结构

代码语言:javascript
复制
[ApiController]
[Route("wecom/callback")]
publicclassWeComController:ControllerBase
{
privatereadonlyILogger<WeComController> _logger;
privatereadonlyWeCom _weCom;

publicWeComController(ILogger<WeComController> logger,IOptionsSnapshot<WeCom> weCom)
{
        _logger = logger;
        _weCom = weCom.Value;
}

// 后续方法实现...
}

4.2 URL验证接口(GET请求)

企业微信在配置回调URL时会发送GET请求进行验证:

代码语言:javascript
复制
[HttpGet]
publicIActionResultGet([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");
returnBadRequest("WeCom config missing");
}

if(string.IsNullOrEmpty(echostr))
returnBadRequest();

// 签名验证
if(!VerifySignature(token, timestamp, nonce, echostr, msg_signature))
{
        _logger.LogWarning("WeCom signature invalid (GET)");
returnBadRequest("signature invalid");
}

try
{
var plain =DecryptMessage(encodingAesKey, echostr, corpId);
// 返回明文完成验证
returnContent(plain,"text/plain",Encoding.UTF8);
}
catch(Exception ex)
{
        _logger.LogWarning(ex,"WeCom decrypt failed (GET)");
returnBadRequest("decrypt failed");
}
}

4.3 消息处理接口(POST请求)

处理企业微信推送的各种事件和消息:

代码语言:javascript
复制
[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");
returnBadRequest();
}

// 读取请求体
usingvar reader =newSystem.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");
returnContent("success","text/plain");
}

// 签名验证
if(!VerifySignature(token, timestamp, nonce, encrypt, msg_signature))
{
        _logger.LogWarning("WeCom signature invalid (POST)");
returnBadRequest("signature invalid");
}

string xml;
try
{
        xml =DecryptMessage(encodingAesKey, encrypt, corpId);
}
catch(Exception ex)
{
        _logger.LogWarning(ex,"WeCom decrypt failed (POST)");
returnBadRequest("decrypt failed");
}

// 处理XML消息内容
// 这里可以添加具体的业务逻辑处理

// 企业微信要求返回固定字符串 "success"
returnContent("success","text/plain");
}

5. 核心工具方法实现

5.1 签名验证

代码语言:javascript
复制
privatestaticboolVerifySignature(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);

usingvar sha1 = SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(raw));
var computed =BitConverter.ToString(hash).Replace("-","").ToLowerInvariant();

returnstring.Equals(computed, signature ??string.Empty,StringComparison.OrdinalIgnoreCase);
}

5.2 消息解密

代码语言:javascript
复制
privatestaticstringDecryptMessage(string encodingAesKey,string inputEncryptBase64,string corpId)
{
// 补齐 base64 填充
var pad = encodingAesKey;
if(pad.Length%4!=0)
        pad +=newstring('=',4- pad.Length%4);
var aesKey =Convert.FromBase64String(pad);

var encrypted =Convert.FromBase64String(inputEncryptBase64);

usingvar aes =Aes.Create();
    aes.Key= aesKey;
    aes.IV = aesKey.Take(16).ToArray();
    aes.Mode=CipherMode.CBC;
    aes.Padding=PaddingMode.None;

usingvar 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)thrownewException("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))
thrownewException("corpId mismatch");
}

return msg;
}

5.3 XML节点提取

代码语言:javascript
复制
privatestaticstringExtractXmlNode(string xml,string nodeName)
{
try
{
var doc =newXmlDocument();
        doc.LoadXml(xml);
var node = doc.SelectSingleNode($"/xml/{nodeName}");
return node?.InnerText;
}
catch
{
returnnull;
}
}

6. 测试与部署

在实际部署和测试时,请确保完成以下步骤:

1.域名验证测试:访问https://your-domain.com/WW_verify_xxxx.txt验证是否返回正确内容2.URL配置:在企业微信后台配置回调URL为https://your-domain.com/wecom/callback3.消息测试:通过企业微信的在线测试工具[1]测试集成效果

7. 最后

本文完整展示了如何使用纯.NET技术栈实现企业微信应用的接入,涵盖了从域名验证到消息处理的完整流程。这种实现方式不依赖任何第三方库,代码简洁清晰,易于理解和维护,适合需要快速接入企业微信的.NET项目使用。您可以根据实际业务需求在此基础上进行扩展,如添加消息类型处理、业务逻辑集成等功能。

References

[1] 在线测试工具: https://developer.work.weixin.qq.com/resource/devtool

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 桑榆肖物 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景
  • 2. 项目结构与配置
    • 2.1 配置文件设置
    • 2.2 配置模型定义
    • 2.3 依赖注入注册
  • 3. 域名归属认证实现
  • 4. 消息接收处理控制器
    • 4.1 控制器基础结构
    • 4.2 URL验证接口(GET请求)
    • 4.3 消息处理接口(POST请求)
  • 5. 核心工具方法实现
    • 5.1 签名验证
    • 5.2 消息解密
    • 5.3 XML节点提取
  • 6. 测试与部署
  • 7. 最后
    • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档