上一篇主要介紹了微信的網(wǎng)頁開發(fā)商乎。這篇來介紹一下微信后臺(tái)開發(fā)。對(duì)于一般的微信運(yùn)營者箫踩,微信公眾平臺(tái)提供了配置自動(dòng)回復(fù)和自定義菜單的功能塑陵。但是對(duì)于開發(fā)人員來說感憾,很多時(shí)候這些基礎(chǔ)的功能并不能滿足我們的需求,比如我們?nèi)绻胍鶕?jù)用戶的不同屬性在用戶關(guān)注的時(shí)候推送不同的圖文令花,基礎(chǔ)的自動(dòng)回復(fù)功能就無法滿足阻桅,這個(gè)時(shí)候就需要引入微信后臺(tái)開發(fā)。
接入微信后臺(tái)開發(fā)的步驟如下:
-
在公眾平臺(tái)的開發(fā)-基本配置里填寫服務(wù)器配置兼都。
微信服務(wù)器配置
-
URL
表示服務(wù)器的回調(diào)地址嫂沉,微信驗(yàn)證服務(wù)器有效性的請(qǐng)求及后續(xù)事件回調(diào)都會(huì)發(fā)到這個(gè)URL上。 -
Token
表示服務(wù)器秘鑰扮碧,用來生成簽名趟章,保存在服務(wù)器端。 -
EncodingAESKey
表示隨機(jī)碼慎王,隨機(jī)生成即可蚓土。 -
消息加解密方式
根據(jù)業(yè)務(wù)需要選擇,一般使用明文模式即可赖淤。
- 驗(yàn)證服務(wù)器地址有效性蜀漆,微信會(huì)發(fā)送一個(gè)帶參數(shù)的GET請(qǐng)求到步驟1填寫的URL上,以此來驗(yàn)證服務(wù)器的有效性漫蛔。這個(gè)驗(yàn)證只有當(dāng)提交服務(wù)器配置的時(shí)候才會(huì)觸發(fā)嗜愈,所以要先把應(yīng)用部署起來再去提交步驟1的服務(wù)器配置。微信驗(yàn)證服務(wù)器地址時(shí)帶的參數(shù)如下:
-
signature
微信加密簽名莽龟,signature結(jié)合了開發(fā)者填寫的token參數(shù)和請(qǐng)求中的timestamp參數(shù)、nonce參數(shù)锨天。 -
timestamp
時(shí)間戳毯盈。 -
nonce
隨機(jī)數(shù)。 -
echostr
隨機(jī)字符串病袄。
開發(fā)者需要對(duì)請(qǐng)求進(jìn)行校驗(yàn)搂赋,來確認(rèn)請(qǐng)求是否來自微信服務(wù)器,如果確認(rèn)成功益缠,則將echostr返回脑奠,服務(wù)器配置既提交成功频蛔。校驗(yàn)的方式如下:
1). 將token檐束、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序生成字符串str1雪营。
2). 將str1進(jìn)行sha1加密產(chǎn)生str2。
3). 比對(duì)str2和signature齿诞,如果一致酸休,則可卻認(rèn)為請(qǐng)求來自微信服務(wù)器,返回echostr即可祷杈。
/**
* 微信簽名驗(yàn)證
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
public static String checkSign(String signature, String timestamp, String nonce, String echostr) {
ApiLogger.weChatLogger.info("微信進(jìn)行接口驗(yàn)證" + StringUtils.join(signature + "; ", timestamp + "; ", nonce + "; ",
echostr + "; "));
String[] strings = {timestamp, nonce, SIGN_TOKEN};
//將timestamp,nonce及token進(jìn)行字典排序
List<String> stringList = Arrays.asList(strings);
Collections.sort(stringList);
String signStr = String.join("", stringList);
//將排序后的串進(jìn)行簽名
String signVal = SignUtil.encode(signStr, SignTypeEnum.SHA1);
if (StringUtils.equalsIgnoreCase(signVal, signature)) {
ApiLogger.weChatLogger.info("微信驗(yàn)證通過" + echostr);
return echostr;
} else {
ApiLogger.weChatLogger.warn("微信驗(yàn)證失敗, 請(qǐng)求中的簽名為 " + signature + "實(shí)際簽名為: " + signVal);
return "failed";
}
}
服務(wù)器驗(yàn)證成功之后就可以按照具體的接口文檔來做開發(fā)了斑司。注意,一旦提交了服務(wù)器配置但汞,公眾平臺(tái)網(wǎng)頁端的自動(dòng)回復(fù)功能和自定義菜單功能就不可用了宿刮。
微信后臺(tái)開發(fā)又主要分為兩部分,被動(dòng)處理微信事件以及主動(dòng)調(diào)用微信接口私蕾。
被動(dòng)處理微信事件
微信對(duì)于公眾號(hào)主動(dòng)觸達(dá)用戶管理的非常嚴(yán)格糙置,除了每個(gè)月限量的群發(fā)機(jī)會(huì)以外,幾乎沒有別的手段可以主動(dòng)發(fā)送消息給用戶(只有格式內(nèi)容要求非常嚴(yán)格的模版消息可以做到)是目。但是微信會(huì)將用戶觸發(fā)的事件以POST的形式發(fā)送到公眾號(hào)配置的服務(wù)器url上谤饭,事件主要分為消息事件、交互事件和卡券事件懊纳。開發(fā)者在接受到這些事件后揉抵,返回響應(yīng)的xml格式,即時(shí)地回復(fù)消息給用戶嗤疯,也可以同時(shí)做一些業(yè)務(wù)邏輯冤今,比如用戶關(guān)注信息入庫之類,不過建議使用異步的方式來處理業(yè)務(wù)邏輯茂缚,避免影響消息的推送戏罢,因?yàn)槿绻⑿欧?wù)器在五秒內(nèi)收不到響應(yīng)會(huì)斷掉連接,并且重新發(fā)起請(qǐng)求脚囊,總共嘗試三次龟糕。這里還有一種介于被動(dòng)和主動(dòng)間的方式,就是服務(wù)器在收到某些事件后悔耘,在一定時(shí)間內(nèi)(48小時(shí))主動(dòng)調(diào)用客服消息接口給用戶發(fā)送客服消息讲岁。如果發(fā)送給用戶的消息依賴于業(yè)務(wù)邏輯,那么建議使用客服消息衬以。
微信事件回調(diào)都是以xml的格式POST到回調(diào)地址的缓艳,所以首先需要解析xml數(shù)據(jù)。這里推薦dom4j看峻,可以很容易地將xml解析為map對(duì)象:
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(String xml) throws DocumentException {
Element ele = DocumentHelper.parseText(xml).getRootElement();
Map<String, String> result = new HashMap<>();
//遍歷所有節(jié)點(diǎn)并存入map
ele.elements().forEach(e -> result.put(((Element) e).getName(), ((Element) e).getText()));
return result;
}
然后根據(jù)xml中的MsgType
屬性來判斷事件類型阶淘,可能出現(xiàn)的值有:event, text, image, voice, video, shortvideo, location, link, news等,上面說到的交互事件和卡券事件都是event類型互妓,而其它的值都是消息事件溪窒,比如text就是用戶發(fā)送文本消息給公眾號(hào)的消息事件坤塞。
接受到事件后,需要做的就根據(jù)需要的消息類型組裝響應(yīng)的xml數(shù)據(jù)霉猛,并且返回給微信服務(wù)器尺锚。目前可以回復(fù)給用戶的信息有文本消息,圖片消息惜浅,語音消息瘫辩,視頻消息,音樂消息和圖文消息坛悉。比如我們現(xiàn)在需要在用戶關(guān)注的時(shí)候推送一個(gè)圖文信息給用戶伐厌,那么我們就需要組裝一個(gè)圖文消息的xml返回給微信服務(wù)器。這里有一點(diǎn)裸影,微信服務(wù)器發(fā)送給我們的xml和我們返回給微信服務(wù)器的xml都有四個(gè)公共屬性挣轨,ToUserName,F(xiàn)romUserName轩猩,CreateTime和MsgType卷扮,所以可以創(chuàng)建一個(gè)抽象基類,然后所有的微信消息都由該基類導(dǎo)出均践。我們?cè)诮M建返回對(duì)象xml的時(shí)候晤锹,只需要把接受到的xml中的ToUserName設(shè)置為返回對(duì)象的FromUserName,F(xiàn)romUserName設(shè)置為ToUserName彤委,其它屬性按照官方文檔設(shè)置即可鞭铆。
將對(duì)象轉(zhuǎn)化為xml可以使用XStream,以下例子是將微信圖文對(duì)象轉(zhuǎn)化為xml的代碼焦影,WeChatNews和WeChatArticle這兩個(gè)類都是自定義的類车遂,用來表示微信圖文和微信圖文里的文章,List<WeChatArticle>是WeChatNews的一個(gè)成員屬性:
public static XStream xStream = new XStream();
/**
* 圖文消息對(duì)象轉(zhuǎn)換成xml
*
* @param news 圖文消息對(duì)象
* @return xml
*/
public static String newsMessageToXml(WeChatNews news) {
xStream.alias("xml", news.getClass());
xStream.alias("item", new WeChatArticle().getClass());
return xStream.toXML(news);
}
由于xml內(nèi)容中可能出現(xiàn)<
或者&
等符號(hào)斯辰,導(dǎo)致xml解析器解析錯(cuò)誤舶担,所以最好將返回對(duì)象xml的值都放在CDATA中〗费模可以通過對(duì)XStream進(jìn)行擴(kuò)展:
private static final CDATA_START = "<![CDATA[";
private static final CDATA_END = "]]>";
/**
* 擴(kuò)展xstream柄沮,使其支持CDATA塊
*/
public static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
protected void writeText(QuickWriter writer, String text) {
writer.write(CDATA_START);
writer.write(text);
writer.write(CDATA_END);
}
};
}
});
P.S. 這里再來吐槽一下微信的官方文檔。我在開發(fā)的時(shí)候遇到過幾處文檔有誤或者文檔缺失的問題:
比如當(dāng)用戶通過微信支付成功后的關(guān)注公眾號(hào)選項(xiàng)關(guān)注時(shí)废岂,微信會(huì)發(fā)送一個(gè)subscribe事件到回調(diào)地址,并且?guī)в衑ventKey(格式為last_trade_no_xxxxxxxxxxx)狱意,而文檔中根本沒有這個(gè)類型的事件的描述湖苞。文檔中只有關(guān)注事件和通過推廣二維碼關(guān)注的事件是subscribe事件,但是前者沒有eventKey详囤,后者eventKey的格式為qrscene_xxxxxx财骨。文檔明顯漏了這一關(guān)注類型镐作。
又比如微信偶爾會(huì)發(fā)一個(gè)不帶參數(shù)的GET請(qǐng)求到回調(diào)地址,這個(gè)就很莫名其妙了隆箩,我翻遍了文檔沒有發(fā)現(xiàn)相關(guān)說明该贾。這種請(qǐng)求只能在代碼中屏蔽掉,問了微信的技術(shù)說是內(nèi)部bug捌臊,不知何時(shí)能解決杨蛋。搜了搜網(wǎng)上也有遇到這個(gè)問題的。
主動(dòng)調(diào)用微信接口
微信后臺(tái)開發(fā)另外一塊很大的內(nèi)容就是主動(dòng)調(diào)用微信接口來實(shí)現(xiàn)一些業(yè)務(wù)邏輯理澎。調(diào)用微信接口首先需要微信基礎(chǔ)服務(wù)access_token逞力。這個(gè)access_token和上一篇講的網(wǎng)頁授權(quán)access_token不一樣,這個(gè)token是調(diào)用微信接口的令牌糠爬。該令牌需要通過請(qǐng)求微信接口獲得:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
注意寇荧,調(diào)用前需要先在微信開發(fā)-基本配置里配置IP白名單,這樣才能調(diào)用獲取access_token接口执隧。
正常返回如下:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
由于獲取令牌的接口每天的調(diào)用次數(shù)有限(100000次)揩抡,所以需要將獲取到access_token保存起來,并且由于令牌兩小時(shí)會(huì)過期镀琉,需要提供主動(dòng)/被動(dòng)的刷新機(jī)制峦嗤。由于線上一般都是分布式的應(yīng)用部署方式,所以為了避免多臺(tái)服務(wù)器同時(shí)去更新access_token滚粟,需要使用分布式鎖寻仗。分布式鎖的實(shí)現(xiàn)方式一般有利用數(shù)據(jù)庫樂觀鎖的,利用redis/memcache的原子性操作的凡壤,以及利用zookeeper的最小節(jié)點(diǎn)的署尤。這里講一下我在代碼中使用的redis的實(shí)現(xiàn)分布式鎖的大致步驟。
- 使用setnx("MyKey", 當(dāng)前時(shí)間+過期超時(shí)時(shí)間) 亚侠,如果返回1曹体,則獲取鎖成功,那么就可以去更新access_token硝烂,并且返回最新的access_token箕别;如果返回0則沒有獲取到鎖,轉(zhuǎn)向2滞谢。
- get("MyKey")獲取值老鎖過期時(shí)間oldExpireTime 串稀,并將這個(gè)值與當(dāng)前的系統(tǒng)時(shí)間進(jìn)行比較,如果大于當(dāng)前系統(tǒng)時(shí)間狮杨,說明鎖還未過期母截,別的請(qǐng)求可能正在更新access_token,直接返回空橄教;如果小于當(dāng)前系統(tǒng)時(shí)間清寇,則認(rèn)為這個(gè)鎖已經(jīng)過期喘漏,那么可以允許別的請(qǐng)求重新獲取,轉(zhuǎn)向3华烟。
- 計(jì)算新鎖過期時(shí)間newExpireTime=當(dāng)前時(shí)間+鎖過期時(shí)間(一個(gè)常量)翩迈,然后使用getset("MyKey", newExpireTime),設(shè)置鎖的新過期時(shí)間為newExpireTime盔夜,這個(gè)操作同時(shí)會(huì)返回當(dāng)前MyKey的值currentExpireTime负饲。
- 判斷鎖當(dāng)前過期時(shí)間currentExpireTime與老鎖過期時(shí)間oldExpireTime是否相等,如果相等比吭,說明獲取鎖成功绽族,那么可以接著更新access_token,并且返回access_token衩藤。如果不相等吧慢,說明這個(gè)鎖又被別的請(qǐng)求獲取走了,因?yàn)樵谶@個(gè)getset操作前已經(jīng)有別的請(qǐng)求執(zhí)行了getset赏表,所以currentExpireTime發(fā)生了變化检诗。那么當(dāng)前請(qǐng)求可以直接返回空。
- 在更新access_token后瓢剿,檢查分布式鎖是否過期逢慌,如果過期則使用delete釋放鎖,否則保留间狂;這里注意一點(diǎn)攻泼,不要在更新access_token后直接刪除鎖,否則會(huì)在高并發(fā)時(shí)鉴象,出現(xiàn)在第2步進(jìn)程獲取老鎖時(shí)為空或者第3步getset獲取當(dāng)前鎖過期時(shí)間為空的情況忙菠。
調(diào)用上述獲取access_token的服務(wù)應(yīng)嘗試三次,直到返回access_token為止纺弊。如果返回為空牛欢,則等待一秒后再獲取,如果三次后如果還未獲取到access_token淆游,則拋出異常傍睹。由于大多數(shù)情況,有效的access_token應(yīng)該在緩存中可以直接取到犹菱,不需要通過分布式鎖更新拾稳,所以不要在同步整個(gè)獲取access_token的服務(wù),負(fù)責(zé)可能會(huì)導(dǎo)致高并發(fā)下的阻塞導(dǎo)致性能瓶頸腊脱。更不能只將更新access_token的邏輯同步熊赖,這樣會(huì)導(dǎo)致重復(fù)更新access_token,而失去了分布式鎖的意義虑椎。
拿到access_token后就可以根據(jù)微信文檔來調(diào)用響應(yīng)的接口了震鹉。這里拿獲取用戶信息接口來舉例。該接口請(qǐng)求說明如下:
http請(qǐng)求方式: GET
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
帶上access_token以及用戶的openid捆姜,就可以向微信請(qǐng)求用戶信息了传趾。HttpClient建議使用RestTemplate,方便簡(jiǎn)潔泥技。
這里又有一個(gè)微信文檔的坑浆兰,獲取用戶信息接口實(shí)際返回的數(shù)據(jù)比文檔中要多一些屬性(應(yīng)該是2018/3/6晚上新增了subscribe_scene, qr_scene和qr_scene_str字段,文檔并沒有及時(shí)更新珊豹。)所以這里需要在轉(zhuǎn)化json到對(duì)象時(shí)簸呈,忽略對(duì)象中沒有的屬性,可以通過配置ObjectMapper對(duì)象來實(shí)現(xiàn):
public class CustomObjectMapper {
public final static ObjectMapper om = new ObjectMapper();
static {
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}
調(diào)用微信接口的代碼:
//微信API接口域名
private static final String API_HOST = "https://api.weixin.qq.com/cgi-bin/";
//10秒鏈接超時(shí)
private static final int CONNECT_TIMEOUT = 10 * 1000;
//1分鐘接收數(shù)據(jù)接收時(shí)間
private static final int READ_TIMEOUT = 60 * 1000;
private RestTemplate restTemplate;
public WeChatServiceImpl() {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(CONNECT_TIMEOUT);
requestFactory.setReadTimeout(READ_TIMEOUT);
this.restTemplate = new RestTemplate(requestFactory);
this.restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
/**
* 通過微信api獲取微信用戶信息
*
* @param openId
* @return
*/
private WeChatUser getWeChatUser(String openId) {
String url = String.format("%suser/info?access_token=%s&openid=%s&lang=zh_CN", API_HOST, weChatTokenService
.getToken().getAccessToken(), openId);
ApiLogger.weChatLogger.info("調(diào)用微信接口獲取微信用戶信息:" + url);
try {
String result = restTemplate.getForObject(url, String.class);
ApiLogger.weChatLogger.debug("調(diào)用微信接口獲取微信用戶信息:" + result);
return CustomObjectMapper.om.readValue(result, WeChatUser.class);
} catch (Exception e) {
String msg = "調(diào)用微信接口獲取微信用戶信息失數瓴琛:" + url;
ApiLogger.weChatLogger.error(msg, e);
throw new WeChatEx(msg, e);
}
}
同時(shí)蜕便,也可以直接到微信公眾平臺(tái)的開發(fā)者工具中,使用接口調(diào)試工具直接調(diào)試和請(qǐng)求接口贩幻。比如生成自定義菜單這種一次性的調(diào)用就非常適合在接口調(diào)試工具中直接調(diào)用轿腺。微信開放了很多有用的接口,比如生成推廣二維碼的接口丛楚,拉取關(guān)注用戶的接口族壳,發(fā)送模版消息的接口,發(fā)送客服消息的接口等等趣些,具體可參照官方文檔仿荆。
花了兩篇文章的篇幅,大概地講了一下微信的網(wǎng)頁開發(fā)和后臺(tái)開發(fā)坏平,也提到了一些我在開發(fā)中遇到的難點(diǎn)和注意點(diǎn)拢操,希望對(duì)大家有用。微信開發(fā)不僅僅包括這兩塊功茴,還有微信jssdk的使用庐冯,微信支付,微信開放平臺(tái)開發(fā)等等坎穿。大家有興趣的話也可以多看看官方文檔展父,雖然里面有不少坑。