微信企业号是微信为企业客户提供的移动服务,旨在提供企业移动应用入口。它可以帮助企业建立员工、上下游供应链与企业IT 系统间的连接。利用企业号,企业或第三方服务商可以快速、低成本的实现高质量的企业移动轻应用,实现生产、管理、协作、运营的移动化。企业号作为企业IT 移动化解决方案,相比企业自己开发APP 具有明显的优势,具体为:
1) 快速移动化办公。企业在开通企业号后,可以直接利用微信及企业号的基础能力,加强员工的沟通与协同,提升企业文化建设、公告通知、知识管理,快速实现企业应用的移动化; 2) 开发成本较低。仅需要按照企业号的标准API 与现有系统进行对接; 3) 零门槛使用。用户微信扫码关注即可使用,在玩微信时,随手处理企业号消息,无需学习即可流畅使用。
企业号是继公众号、订阅号的另外一种微信类型,它主要是面对企业的。企业号是微信为企业客户提供的移动应用入口。可以帮助企业建立员工、上下游供应链与企业 IT 系统间的连接。利用 企业号 ,企业或第三方合作伙伴可以帮助企业快速、低成本的实现高质量的移动轻应用,实现生产、管理、协作、运营的 移动化 。企业号最大的亮点是可以不限数量的消息发送,也就是可以在企业员工之间畅通交流。相对于公众号和订阅号,发送消息的谨慎程度,微信企业号可谓给人眼前一亮的感觉。不过微信企业号是需要内部建立好通讯录,关注者需要匹配通讯录的微信号、邮箱、电话号码任一个通过才可以关注,也就是可以防止其他外来人员的自由关注了,另外如果为了安全考虑,还可以设置二次验证,也就是一个审核过程。企业号的认证和公众号一样,需要提供相关的企业资质文件,并且认证每年都要收取费用,否则可能有人员和功能的一些限制。
开启“回调模式”:
在回调模式下,企业不仅可以主动调用企业号接口,还可以接收成员的消息或事件。接收的信息使用XML数据格式、UTF8编码,并以AES方式加密。
获取CorpID(企业号编号)
在权限管理中新建管理组并分配权限,从而获取企业的CorPID(企业编号相当于唯一的企业的账号),Secret(开发者凭据)。
如果开发过微信企业号,那么我们就知道,如果需要在微信服务器和网站服务器之间建立连接关系,实现消息的转发和处理,那么就应该设置一个回调模式,需要配置好相关的参数。然后在自己 网站服务器里面建立一个处理微信服务器消息的入口。
企业号在回调企业URL时,会对消息体本身做AES加密,以XML格式POST到企业应用的URL上;企业在被动响应时,也需要对数据加密,以XML格式返回给微信。企业的回复支持文本、图片、语音、视频、图文等格式。
设置URL、Token和AESKey
进入配置后,我们需要修改相关的URL、Token、EncodingAESKey等参数,主要是URL,需要我们发布到网站服务器上的处理入口。
Token和AESKey可以根据提示动态生成一个即可,AESKey好像必须是23位的,所以这个一般是让它自己生成的,这个主要用来加密解密使用的。
1)URL是企业应用接收企业号推送请求的访问协议和地址,支持http或https协议。
2)Token可由企业任意填写,用于生成签名。
3)EncodingAESKey用于消息体的加密,是AES密钥的Base64编码。
验证URL、Token以及加密的详细处理请参考后续 “接收消息时的加解密处理” 的部分。
回调模式接口
/// <summary>
/// 企业号回调信息接口。统一接收并处理信息的入口。
/// </summary>
public class corpapi : IHttpHandler
{
/// <summary>
/// 处理企业号的信息
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpContext context)
{
string postString = string.Empty;
//判断请求类型
if (HttpContext.Current.Request.HttpMethod.ToUpper() == "POST")
{
using (Stream stream = HttpContext.Current.Request.InputStream)
{
Byte[] postBytes = new Byte[stream.Length];
stream.Read(postBytes, 0, (Int32)stream.Length);
postString = Encoding.UTF8.GetString(postBytes);
}
if (!string.IsNullOrEmpty(postString))
{
Execute(postString);
}
}
else
{
Auth();
}
}
/// <summary>
/// 成为开发者的第一步,验证并相应服务器的数据
/// </summary>
private void Auth()
{
#region 获取关键参数
string token = ConfigurationManager.AppSettings["CorpToken"];//从配置文件获取Token
if (string.IsNullOrEmpty(token))
{
LogTextHelper.Error(string.Format("CorpToken 配置项没有配置!"));
}
string encodingAESKey = ConfigurationManager.AppSettings["EncodingAESKey"];//从配置文件获取EncodingAESKey
if (string.IsNullOrEmpty(encodingAESKey))
{
LogTextHelper.Error(string.Format("EncodingAESKey 配置项没有配置!"));
}
string corpId = ConfigurationManager.AppSettings["CorpId"];//从配置文件获取corpId
if (string.IsNullOrEmpty(corpId))
{
LogTextHelper.Error(string.Format("CorpId 配置项没有配置!"));
}
#endregion
string echoString = HttpContext.Current.Request.QueryString["echoStr"];
string signature = HttpContext.Current.Request.QueryString["msg_signature"];//企业号的 msg_signature
string timestamp = HttpContext.Current.Request.QueryString["timestamp"];//时间戳
string nonce = HttpContext.Current.Request.QueryString["nonce"];//随机数
string decryptEchoString = "";
if (new CorpBasicApi().CheckSignature(token, signature, timestamp, nonce, corpId, encodingAESKey, echoString, ref decryptEchoString))
{
if (!string.IsNullOrEmpty(decryptEchoString))
{
HttpContext.Current.Response.Write(decryptEchoString);
HttpContext.Current.Response.End();
}
}
}
接收消息时的加解密处理
以下是一个加解密处理的类是微信企业号附录里面提供的,我使用了C#版本的SDK而已。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Collections;
using System.Security.Cryptography;
//-40001 : 签名验证错误
//-40002 : xml解析失败
//-40003 : sha加密生成签名失败
//-40004 : AESKey 非法
//-40005 : corpid 校验错误
//-40006 : AES 加密失败
//-40007 : AES 解密失败
//-40008 : 解密后得到的buffer非法
//-40009 : base64加密异常
//-40010 : base64解密异常
namespace Tencent
{
class WXBizMsgCrypt
{
string m_sToken;
string m_sEncodingAESKey;
string m_sCorpID;
enum WXBizMsgCryptErrorCode
{
WXBizMsgCrypt_OK = 0,
WXBizMsgCrypt_ValidateSignature_Error = -40001,
WXBizMsgCrypt_ParseXml_Error = -40002,
WXBizMsgCrypt_ComputeSignature_Error = -40003,
WXBizMsgCrypt_IllegalAesKey = -40004,
WXBizMsgCrypt_ValidateCorpid_Error = -40005,
WXBizMsgCrypt_EncryptAES_Error = -40006,
WXBizMsgCrypt_DecryptAES_Error = -40007,
WXBizMsgCrypt_IllegalBuffer = -40008,
WXBizMsgCrypt_EncodeBase64_Error = -40009,
WXBizMsgCrypt_DecodeBase64_Error = -40010
};
//构造函数
// @param sToken: 公众平台上,开发者设置的Token
// @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey
// @param sCorpID: 企业号的CorpID
public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sCorpID)
{
m_sToken = sToken;
m_sCorpID = sCorpID;
m_sEncodingAESKey = sEncodingAESKey;
}
//验证URL
// @param sMsgSignature: 签名串,对应URL参数的msg_signature
// @param sTimeStamp: 时间戳,对应URL参数的timestamp
// @param sNonce: 随机串,对应URL参数的nonce
// @param sEchoStr: 随机串,对应URL参数的echostr
// @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
// @return:成功0,失败返回对应的错误码
public int VerifyURL(string sMsgSignature, string sTimeStamp, string sNonce, string sEchoStr, ref string sReplyEchoStr)
{
int ret = 0;
if (m_sEncodingAESKey.Length!=43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEchoStr, sMsgSignature);
if (0 != ret)
{
return ret;
}
sReplyEchoStr = "";
string cpid = "";
try
{
sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid); //m_sCorpID);
}
catch (Exception)
{
sReplyEchoStr = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
}
if (cpid != m_sCorpID)
{
sReplyEchoStr = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
}
return 0;
}
// 检验消息的真实性,并且获取解密后的明文
// @param sMsgSignature: 签名串,对应URL参数的msg_signature
// @param sTimeStamp: 时间戳,对应URL参数的timestamp
// @param sNonce: 随机串,对应URL参数的nonce
// @param sPostData: 密文,对应POST请求的数据
// @param sMsg: 解密后的原文,当return返回0时有效
// @return: 成功0,失败返回对应的错误码
public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg)
{
if (m_sEncodingAESKey.Length!=43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
XmlDocument doc = new XmlDocument();
XmlNode root;
string sEncryptMsg;
try
{
doc.LoadXml(sPostData);
root = doc.FirstChild;
sEncryptMsg = root["Encrypt"].InnerText;
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error;
}
//verify signature
int ret = 0;
ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature);
if (ret != 0)
return ret;
//decrypt
string cpid = "";
try
{
sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid);
}
catch (FormatException)
{
sMsg = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error;
}
catch (Exception)
{
sMsg = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
}
if (cpid != m_sCorpID)
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
return 0;
}
//将企业号回复用户的消息加密打包
// @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
// @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp
// @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
// @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
// 当return返回0时有效
// return:成功0,失败返回对应的错误码
public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg)
{
if (m_sEncodingAESKey.Length!=43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
string raw = "";
try
{
raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sCorpID);
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error;
}
string MsgSigature = "";
int ret = 0;
ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature);
if (0 != ret)
return ret;
sEncryptMsg = "";
string EncryptLabelHead = "<Encrypt><![CDATA[";
string EncryptLabelTail = "]]></Encrypt>";
string MsgSigLabelHead = "<MsgSignature><![CDATA[";
string MsgSigLabelTail = "]]></MsgSignature>";
string TimeStampLabelHead = "<TimeStamp><![CDATA[";
string TimeStampLabelTail = "]]></TimeStamp>";
string NonceLabelHead = "<Nonce><![CDATA[";
string NonceLabelTail = "]]></Nonce>";
sEncryptMsg = sEncryptMsg + "<xml>" + EncryptLabelHead + raw + EncryptLabelTail;
sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail;
sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail;
sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail;
sEncryptMsg += "</xml>";
return 0;
}
public class DictionarySort : System.Collections.IComparer
{
public int Compare(object oLeft, object oRight)
{
string sLeft = oLeft as string;
string sRight = oRight as string;
int iLeftLength = sLeft.Length;
int iRightLength = sRight.Length;
int index = 0;
while (index < iLeftLength && index < iRightLength)
{
if (sLeft[index] < sRight[index])
return -1;
else if (sLeft[index] > sRight[index])
return 1;
else
index++;
}
return iLeftLength - iRightLength;
}
}
//Verify Signature
private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture)
{
string hash = "";
int ret = 0;
ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash);
if (ret != 0)
return ret;
if (hash == sSigture)
return 0;
else
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error;
}
}
public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature)
{
ArrayList AL = new ArrayList();
AL.Add(sToken);
AL.Add(sTimeStamp);
AL.Add(sNonce);
AL.Add(sMsgEncrypt);
AL.Sort(new DictionarySort());
string raw = "";
for (int i = 0; i < AL.Count; ++i)
{
raw += AL[i];
}
SHA1 sha;
ASCIIEncoding enc;
string hash = "";
try
{
sha = new SHA1CryptoServiceProvider();
enc = new ASCIIEncoding();
byte[] dataToHash = enc.GetBytes(raw);
byte[] dataHashed = sha.ComputeHash(dataToHash);
hash = BitConverter.ToString(dataHashed).Replace("-", "");
hash = hash.ToLower();
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;
}
sMsgSignature = hash;
return 0;
}
}
}