前言
- 2019年我們公司的新的業(yè)務(wù)為了打通微信生態(tài)開始接入公眾號(hào)废膘,小程序的開發(fā),到今年四月又開始打通企業(yè)微信生態(tài)般又。微信當(dāng)時(shí)有的文檔特別的坑彼绷,坑到爆炸(可能現(xiàn)在已經(jīng)改正,暫無(wú)查證)茴迁。中間的開發(fā)工作也遇到很多的問(wèn)題苛预,耽誤了很多的時(shí)間。所以把問(wèn)題總結(jié)一下笋熬,防止后面犯類似的錯(cuò)誤。
一:微信的UnionID的獲取問(wèn)題
1:?jiǎn)栴}
- 當(dāng)我們開發(fā)一個(gè)新的小程序并且需要和以前的公眾號(hào)的用戶打通的時(shí)候腻菇,我們發(fā)現(xiàn)了我們小程序的用戶拿不到unionid胳螟,無(wú)法和以前的公眾號(hào)做關(guān)聯(lián)
- 還有個(gè)問(wèn)題就是我們的公眾號(hào)測(cè)試環(huán)境的用戶也沒(méi)法和生產(chǎn)的數(shù)據(jù)進(jìn)行打通。當(dāng)時(shí)的微信文檔只看到一個(gè)unionid的機(jī)制筹吐,沒(méi)有具體的頁(yè)面操作流程糖耸。
2:原因
- UnionId 是微信給一個(gè)用戶做唯一識(shí)別的標(biāo)志,是用戶微信唯一的id丘薛。如果企業(yè)只有一個(gè)公眾號(hào)的時(shí)候嘉竟,我們是無(wú)法獲取到UnionID的,因?yàn)橹辽僖袃蓚€(gè)及以上的應(yīng)用(公眾號(hào)洋侨,小程序舍扰,企業(yè)微信,app應(yīng)用希坚,網(wǎng)站三方應(yīng)用)在微信開放平臺(tái)上進(jìn)行關(guān)聯(lián)才可以獲取到unionID
3:解決方案
- 在微信開放平臺(tái)綁定主體(公眾號(hào)边苹,小程序),如果綁定的應(yīng)用在同一個(gè)主體下面裁僧,我們就可以獲取到用戶的unionId
微信開放平臺(tái)綁定小程序和公眾號(hào)
- 開發(fā)者平臺(tái)綁定的小程序也可以是體驗(yàn)版的小程序个束,綁定上以后我們的測(cè)試環(huán)境的用戶就能和公眾號(hào)的用戶(包含生產(chǎn)和測(cè)試環(huán)境)的用戶打通
- 企業(yè)微信綁定小程序要到企業(yè)微信平臺(tái)設(shè)置慕购。企業(yè)微信->應(yīng)用管理->小程序->關(guān)聯(lián)小程序
- 企業(yè)微信的關(guān)聯(lián)應(yīng)用要到企業(yè)微信->應(yīng)用管理->應(yīng)用->創(chuàng)建應(yīng)用設(shè)置,如果已有應(yīng)用要在第三方->添加第三方應(yīng)用設(shè)置
二:部分用戶拿不到unionId的問(wèn)題
1:?jiǎn)栴}
- 在我們的小程序授權(quán)登錄的時(shí)候茬底,有一部分用戶拿不到微信的unionId沪悲,導(dǎo)致我們整個(gè)流程進(jìn)行不下去,查了很多的資料阱表,都是從wx.getUserInfo()里獲取殿如,但是取的解密值是沒(méi)有unionId字段的,如下
{"phoneNumber":"15251***648","watermark":{"appid":"wx4b101b***1f6b108","timestamp":1586253485},"purePhoneNumber":"15251**648","countryCode":"86"}
2:原因:
- 暫時(shí)還沒(méi)有明白微信的wx.getUserInfo什么情況下會(huì)返回unionId的值
3:解決方案:
- 前端如果傳的微信code授權(quán)獲取不到unionId捶枢,那么就要從前端第一次登錄授權(quán)獲取到的加密數(shù)據(jù)encryptedData和iv傳到后臺(tái)握截,然后后端再次登錄授權(quán)獲取到session_key 進(jìn)行解密,就能獲取到unionId
- 獲取用戶session_key和unionId代碼如下
public String getUnionId(String code,String iv,String encryptedData) {
RestTemplate restTemplate = new RestTemplate();
StringBuilder unionIdRequestUrl = new StringBuilder(URI_UNIONID);
unionIdRequestUrl.append("?appid="+appid);
unionIdRequestUrl.append("&secret="+secret);
unionIdRequestUrl.append("&js_code="+code);
unionIdRequestUrl.append("&grant_type=authorization_code");
String response = restTemplate.getForObject(unionIdRequestUrl.toString(), String.class);
Map<String, Object> result = new ObjectMapper().readValue(response, Map.class);
String sessionKey = MapUtils.getString(result, "session_key");
String unionId = MapUtils.getString(result, "unionId");
// 如果沒(méi)有獲取到就解析前端的加密數(shù)據(jù)
if (null != unionId) {
String str = AES.wxDecrypt(encryptedData, sessionKey, iv);
JSONObject obj = JSONObject.parseObject(str);
unionId = null != obj?obj.getString("unionId"):null;
}
return unionId
}
三:企業(yè)微信的簽名機(jī)制和配置config獲取問(wèn)題
1:?jiǎn)栴}
- 在企業(yè)微信開發(fā)中烂叔,我們一個(gè)需求是需要獲取當(dāng)前聊天人的信息然后入庫(kù)保存谨胞。我們制定的步驟就是前端先通過(guò)客戶端的API獲取當(dāng)前聊天人的userId,然后再通過(guò)服務(wù)端API獲取用戶的信息蒜鸡。在前端獲取客戶端api的時(shí)候必須要先獲取權(quán)限config的配置胯努,這個(gè)后端提供了接口給前端,包含簽名逢防,生成簽名的隨機(jī)串等信息叶沛,如圖
- 但是前端通過(guò)這個(gè)獲取簽名獲取了userId以后,再調(diào)用后端接口的時(shí)候總會(huì)報(bào)非法的簽名信息忘朝。
2:原因
- 企業(yè)微信的調(diào)用應(yīng)用接口的權(quán)限分為兩步:一是獲取權(quán)限驗(yàn)證的配置 二是獲取應(yīng)用的權(quán)限的配置
- 我們通過(guò)我們的后端只返回給了前端一次簽名讓其去獲取權(quán)限驗(yàn)證的config灰署,然后前端又去調(diào)企業(yè)微信的接口,這個(gè)是行不通的局嘁,必須再獲取一次應(yīng)用的權(quán)限配置config溉箕,也就是要從后端獲取兩次簽名,用兩次不同的簽名獲取不同的config
- 企業(yè)微信的config必須通過(guò)簽名去獲取悦昵,簽名由后端生成返回給前端
3:解決方案(java后端)
//企業(yè)微信的jsapi_ticket地址
private final static String GET_WX_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN";
//應(yīng)用tictet地址
private final static String GET_APP_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=ACCESS_TOKEN&type=agent_config";public Object getShareSign(@RequestBody ReqWeiXinShare req) Exception {
// 微信唯一標(biāo)識(shí)
String appid = req.getAppid();
// 微信調(diào)用接口憑證
String appSecret = req.getAppSecret();
// 分享url
String locationUrl = req.getLocationUrl();
if (!StringUtils.isEmpty(appid) && !StringUtils.isEmpty(appSecret)
&& !StringUtils.isEmpty(locationUrl)) {
String accessToken = wxCpUserService.accessToken();
Map<String,Object> resultMap = new HashMap<>();
Map<String, String> jsApiMap = this.sign(GET_WX_TICKET, accessToken,req.getLocationUrl());
if (null != jsApiMap) {
resultMap.put("config_nonceStr",jsApiMap.get("nonceStr"));
resultMap.put("config_ticket",jsApiMap.get("ticket"));
resultMap.put("config_timestamp",jsApiMap.get("timestamp"));
resultMap.put("config_signature",jsApiMap.get("signature"));
}
Map<String, String> ticketMap = this.sign(GET_APP_TICKET, accessToken,req.getLocationUrl());
if (null != ticketMap) {
resultMap.put("agent_nonceStr",ticketMap.get("nonceStr"));
resultMap.put("agent_ticket",ticketMap.get("ticket"));
resultMap.put("agent_timestamp",ticketMap.get("timestamp"));
resultMap.put("agent_signature",ticketMap.get("signature"));
}
resultMap.put("appid", appid);
resultMap.put("url", locationUrl);
return resultMap;
} else {
return null;
}
}
private Map<String, String> sign(String url, String accessToken,String redirectUrl) {
String ticket = this.getTicket(url,accessToken);
Map<String, String> ret = new HashMap<String, String>();
String noncestr = getRandomString(16);
String timestamp = Long.toString(System.currentTimeMillis()).substring(0,10);
String signature = "";
String params = "jsapi_ticket=" + ticket + "&noncestr=" + noncestr
+ "×tamp=" + timestamp + "&url=" + redirectUrl;
log.info("簽名返回內(nèi)容:{}", params);
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(params.getBytes("UTF-8"));
signature = this.byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException var10) {
var10.printStackTrace();
} catch (UnsupportedEncodingException var11) {
var11.printStackTrace();
}
ret.put("url", url);
ret.put("ticket", ticket);
ret.put("nonceStr", noncestr);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
return ret;
}
private String getTicket(String url,String accessToke) {
String ticket = "";
RestTemplate restTemplate = new RestTemplate();
url = url.replace("ACCESS_TOKEN",accessToke);
try {
String response = restTemplate.getForObject(url, String.class);
JSONObject demoJson = JSON.parseObject(response);
if (demoJson.getString("errcode").equals("40001")) {
return "error";
}
ticket = demoJson.getString("ticket");
System.out.println(ticket);
} catch (Exception var6) {
var6.printStackTrace();
}
return ticket;
}
private static String getRandomString(int length){
String keyString = "ergrfewfwdgggcvv;uihefujsncjdvngrjegeuirgverggvbergbvuigverug";
int len = keyString.length();
StringBuffer str = new StringBuffer();
for(int i=0;i<length;i++){
str.append(keyString.charAt((int) Math.round(Math.random() * (len - 1))));
}
return str.toString();
}
private String byteToHex(byte[] hash) {
Formatter formatter = new Formatter();
byte[] var3 = hash;
int var4 = hash.length;
for (int var5 = 0; var5 < var4; ++var5) {
byte b = var3[var5];
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
- 6:前端根據(jù)返回的兩個(gè)config配置肴茄,去請(qǐng)求企業(yè)微信的接口,就能調(diào)的通了
三:總結(jié)
- 微信的開發(fā)主要是因?yàn)槲臋n不清楚不連貫的導(dǎo)致開發(fā)者浪費(fèi)了大量的時(shí)間但指,我們遇到的主要問(wèn)題一個(gè)unionId的獲取不到寡痰,不知道如何配置才能獲取到unionId,那個(gè)時(shí)候微信文檔也沒(méi)有介紹(貌似現(xiàn)在有了)棋凳,一個(gè)前端企業(yè)微信接口調(diào)不通問(wèn)題拦坠,不清楚還要兩次獲取config,如何獲取config的簽名微信的文檔也沒(méi)有說(shuō)明怎么獲取剩岳,只說(shuō)了一個(gè)必填贪婉。解決了這些問(wèn)題我們的后期開發(fā)也順利了很多,希望以后騰訊系能更多支持java生態(tài)卢肃,封裝一些api的sdk給我們