微信企业号是微信为企业客户提供的移动服务,旨在提供企业移动应用入口。它可以帮助企业建立员工、上下游供应链与企业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"];//从配置文件获取Tokenif (string.IsNullOrEmpty(token)){LogTextHelper.Error(string.Format("CorpToken 配置项没有配置!"));}string encodingAESKey = ConfigurationManager.AppSettings["EncodingAESKey"];//从配置文件获取EncodingAESKeyif (string.IsNullOrEmpty(encodingAESKey)){LogTextHelper.Error(string.Format("EncodingAESKey 配置项没有配置!"));}string corpId = ConfigurationManager.AppSettings["CorpId"];//从配置文件获取corpIdif (string.IsNullOrEmpty(corpId)){LogTextHelper.Error(string.Format("CorpId 配置项没有配置!"));} #endregionstring echoString = HttpContext.Current.Request.QueryString["echoStr"];string signature = HttpContext.Current.Request.QueryString["msg_signature"];//企业号的 msg_signaturestring 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: 企业号的CorpIDpublic 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 signatureint ret = 0;ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature);if (ret != 0)return ret;//decryptstring 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;elseindex++;}return iLeftLength - iRightLength;}}//Verify Signatureprivate 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 = puteHash(dataToHash);hash = BitConverter.ToString(dataHashed).Replace("-", "");hash = hash.ToLower();}catch (Exception){return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;}sMsgSignature = hash;return 0;}}}