本文主要介绍微信公众号(服务号)开发的简要流程和常见问题,官方版的公众号开发文档请见地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1445241432
3 常用开发接口
微信公众号常用的开发接口功能包括:微信网页授权、微信JS-SDK和图文消息交互等,下面将进行简要介绍。
3.1 微信网页授权
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
3.1.1 关于网页授权回调域名的说明
(1)在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
(2)授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com无法进行OAuth2.0鉴权
3.1.2 关于网页授权的两种scope的区别说明
(1)以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
(2)以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
(3)用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。
3.1.3 网页授权配置步骤
(1) 配置授权入口地址
(2) 用户同意授权,获取code
如2.4.3小节示例,配置授权地址(“发送包”按钮):
http://open.weixin.qq.com/connect/oauth2/authorize?appid=wxe649dc08bf553be2&redirect_uri=https%3a%2f%2fwww.mydomain.com%2fwechatstock%2ftrade%2fsendform.do&response_type=code&scope=snsapi_base&state=123#wechat_redirect
用户点击此按钮后,微信后台将调用回调地址:
https://www.mydomain.com/wechatstock/trade/sendform.do?code=CODE&state=STATE
code说明 : code作为换取授权access_token(此access_token与公众号通用的access_token不一样)的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。state参数可以忽略。
(3) 通过code换取网页授权access_token
如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。
请求方法
获取code后,请求以下链接获取access_token:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
code :填写第一步获取的code参数
正确时返回的JSON数据包如下:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
此时,公众号已获取到用户的openID,即openid字段。如果需要获取更多的用户信息,如昵称、头像和地址等,则需要继续进行(4)~(5)步。
(4) 刷新access_token(如果需要)
由于access_token拥有较短的有效期,当access_token超时后,可以使用refresh_token进行刷新,refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权。
请求方法
获取第二步的refresh_token后,请求以下链接获取access_token:
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
正确时返回的JSON数据包如下:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
(5) 拉取用户信息(需scope为 snsapi_userinfo)
如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。
请求方法
http:GET(请使用https协议) :
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
正确时返回的JSON数据包如下:
{
"openid":" OPENID",
" nickname": NICKNAME,
"sex":"1",
"province":"PROVINCE"
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
3.2 微信JS-SDK
微信JS-SDK是微信公众平台 面向网页开发者提供的基于微信内的网页开发工具包。
通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
3.2.1 JSSDK使用步骤
(1)绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
备注:登录后可在“开发者中心”查看对应的接口权限。
示例:
(2)引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):
http://res.wx.qq.com/open/js/jweixin-1.2.0.js
(3)通过config接口注入权限验证配置
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});
(4)通过ready接口处理成功验证
wx.ready(function(){
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});
(5)通过error接口处理失败验证
wx.error(function(res){
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});
3.2.2 JS-SDK使用权限签名
(1)关于jsapi_ticket
生成签名之前必须先了解一下jsapi_ticket。jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。
步骤一:参考2.3.1获取基础access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token)。
步骤二:用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
成功返回如下JSON:
{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}
获得jsapi_ticket之后,就可以生成JS-SDK权限验证的签名了。
(2)JS-SDK权限验证的签名算法
签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
即signature=sha1(string1)。 示例:
noncestr=Wm3WZYTPz0wzccnW
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
timestamp=1414587457
url=http://mp.weixin.qq.com?params=value
步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value
步骤2. 对string1进行sha1签名,得到signature:
0f9de62fce790f9a083d5c99e95740ceb90c27ed
示例代码:
/**
* 获取JSSDK配置
*
* @param url
* @return JsSdkConfigVO
*/
public JsSdkConfigVO getJsSdkConfig(String url) {
LOGGER.debug("get jssdk config");
JsSdkConfigVO jsSdkConfigVO = new JsSdkConfigVO();
try {
String nonceStr = StringUtil.genRandStr(24);
String timestamp = Long.toString(System.currentTimeMillis() / 1000);
String jsApiTicket = getJsApiTicket();
// 注意这里参数名必须全部小写,且必须有序
String plainPattern = "jsapi_ticket={0}&noncestr={1}×tamp={2}&url={3}";
String plainStr = MessageFormat.format(plainPattern, jsApiTicket, nonceStr, timestamp, url);
LOGGER.debug("jssdk plainStr:{}", plainStr);
MessageDigest encryptor = MessageDigest.getInstance("SHA-1");
encryptor.reset();
encryptor.update(plainStr.getBytes(CharsetConstants.UTF8));
String signature = byteToHex(encryptor.digest());
LOGGER.debug("jssdk signature:{}", signature);
jsSdkConfigVO.setAppId(appId);
jsSdkConfigVO.setNonceStr(nonceStr);
jsSdkConfigVO.setSignature(signature);
jsSdkConfigVO.setTimestamp(timestamp);
} catch (Exception e) {
LOGGER.error("获取微信js接口授权失败", e);
}
return jsSdkConfigVO;
}
3.3 图文消息交互
3.3.1 接收普通消息
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)。关于消息加解密的详细说明,请见“发送消息-被动回复消息加解密说明”。
主要消息类型的推送XML数据包结构如下:
(1)文本消息
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[fromUser] ]>
</FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType>
< ![CDATA[text] ]>
</MsgType>
<Content>
< ![CDATA[this is a test] ]>
</Content>
<MsgId>1234567890123456</MsgId>
</xml>
(2)图片消息
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[fromUser] ]>
</FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType>
< ![CDATA[image] ]>
</MsgType>
<PicUrl>
< ![CDATA[this is a url] ]>
</PicUrl>
<MediaId>
< ![CDATA[media_id] ]>
</MediaId>
<MsgId>1234567890123456</MsgId>
</xml>
3.3.2 接收事件推送
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许。主要的事件推送如下:
(1)关注/取消关注事件
用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。
关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。
推送XML数据包示例:
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[FromUser] ]>
</FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType>
< ![CDATA[event] ]>
</MsgType>
<Event>
< ![CDATA[subscribe] ]>
</Event>
</xml>
(2)自定义菜单事件
用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。
点击菜单拉取消息时的事件推送
推送XML数据包示例:
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[FromUser] ]>
</FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType>
< ![CDATA[event] ]>
</MsgType>
<Event>
< ![CDATA[CLICK] ]>
</Event>
<EventKey>
< ![CDATA[EVENTKEY] ]>
</EventKey>
</xml>
点击菜单跳转链接时的事件推送
推送XML数据包示例:
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[FromUser] ]>
</FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType>
< ![CDATA[event] ]>
</MsgType>
<Event>
< ![CDATA[VIEW] ]>
</Event>
<EventKey>
< ![CDATA[www.qq.com] ]>
</EventKey>
</xml>
3.3.3 被动回复用户消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
主要回复消息类型如下:
(1)回复文本消息
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[fromUser] ]>
</FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType>
< ![CDATA[text] ]>
</MsgType>
<Content>
< ![CDATA[你好] ]>
</Content>
</xml>
(2)回复图片消息
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[fromUser] ]>
</FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType>
< ![CDATA[image] ]>
</MsgType>
<Image>
<MediaId>
< ![CDATA[media_id] ]>
</MediaId>
</Image>
</xml>
(3)回复语音消息
<xml>
<ToUserName>
< ![CDATA[toUser] ]>
</ToUserName>
<FromUserName>
< ![CDATA[fromUser] ]>
</FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType>
< ![CDATA[voice] ]>
</MsgType>
<Voice>
<MediaId>
< ![CDATA[media_id] ]>
</MediaId>
</Voice>
</xml>
3.3.4 图文消息接收和发送示例代码
示例代码:
/**
* 微信开放平台的后台消息响应入口
*
* @return
*/
@RequestMapping(value = "/mobile/wx/wx.do", method = RequestMethod.POST)
@ResponseBody
public String wechatDataHandler(HttpServletRequest request) {
String response = null;
try {
LOGGER.debug("receive wechat data POST request.");
// 校验微信后台请求数据
String nonce = RequestUtils.getNeedString(request, "nonce", "nonce is empty!");
String signature = RequestUtils.getNeedString(request, "signature", "signature is empty!");
String timestamp = RequestUtils.getNeedString(request, "timestamp", "timestamp is empty!");
LOGGER.debug("nonce is: {}", nonce);
LOGGER.debug("signature is: {}", signature);
LOGGER.debug("timestamp is: {}", timestamp);
String[] array = new String[] { MobileConfig.getWechatToken(), timestamp, nonce };
Arrays.sort(array);
StringBuilder sb = new StringBuilder();
for (String str : array) {
sb.append(str);
}
String md = CrypUtil.encryptHashStr(sb.toString(), "SHA1");
LOGGER.debug("md is: {}", md);
if (!md.equalsIgnoreCase(signature)) {
throw new Exception("md and signature not match!");
} else {
LOGGER.debug("md and signature match.");
}
// 数据获取、处理与响应
WechatDataVO requestData = getDataFormRequest(request);
WechatDataVO responseData = handleRequestData(requestData);
response = getResponseStr(responseData);
} catch (Exception e) {
LOGGER.error("wechat handle request failed!", e);
// 微信约定的响应字符串,"success"表示不处理或者处理失败
response = "success";
}
LOGGER.debug("wechat response string: {}", response);
return response;
}
/**
* 获取微信后台数据
*
* @param request HttpServletRequest
* @return
*/
private WechatDataVO getDataFormRequest(HttpServletRequest request) {
WechatDataVO requestData = new WechatDataVO();
try {
InputStream is = request.getInputStream();
if (is != null) {
SAXReader reader = new SAXReader();
Document document = reader.read(is);
LOGGER.debug("request data asXML: {}", document.asXML());
Element root = document.getRootElement();
// 公众号
String toUserName = root.elementText("ToUserName");
// 粉丝号
String fromUserName = root.elementText("FromUserName");
// 消息类型
String msgType = root.elementText("MsgType");
// 微信后台消息ID
String msgId = root.elementText("MsgId");
// 消息内容
String content = root.elementText("Content");
// 消息时间
String createTime = root.elementText("CreateTime");
// 图片url
String picUrl = root.elementText("PicUrl");
// 图片id
String mediaId = root.elementText("MediaId");
// 事件类型
String event = root.elementText("Event");
// 事件KEY值
String eventKey = root.elementText("EventKey");
requestData.setContent(content);
requestData.setPicUrl(picUrl);
requestData.setMediaId(mediaId);
requestData.setCreateTime(createTime);
requestData.setFromUserName(fromUserName);
requestData.setMsgId(msgId);
requestData.setMsgType(msgType);
requestData.setToUserName(toUserName);
requestData.setEvent(event);
requestData.setEventKey(eventKey);
} else {
LOGGER.debug("InputStream is empty!");
}
} catch (Exception e) {
LOGGER.error("parse wechat post data error! ", e);
}
LOGGER.debug("requestData is: {}", requestData.toDataString());
return requestData;
}
/**
* 处理接收到的微信后台数据
*
* @param requestData
* @return
* @throws Exception
*/
private WechatDataVO handleRequestData(WechatDataVO requestData) throws Exception {
WechatDataVO responseData = new WechatDataVO();
String createTime = DateTimeUtils.getTodayStr(DateFormatEnum.PATTERN_4);
String msgType = requestData.getMsgType();
responseData.setCreateTime(createTime);
if (WechatConstant.MSG_TYPE_TEXT.equals(msgType)) {
responseData.setContent(requestData.getContent());
responseData.setMsgType(msgType);
} else if (WechatConstant.MSG_TYPE_IMAGE.equals(msgType)) {
responseData.setPicUrl(requestData.getPicUrl());
responseData.setMediaId(requestData.getMediaId());
responseData.setMsgType(msgType);
} else if (WechatConstant.MSG_TYPE_EVENT.equals(msgType)) {
responseData.setContent("欢迎关注");
responseData.setMsgType(WechatConstant.MSG_TYPE_TEXT);
} else {
throw new Exception("not supported MsgType: " + msgType);
}
responseData.setFromUserName(requestData.getToUserName());
responseData.setToUserName(requestData.getFromUserName());
LOGGER.debug("responseData is: {}", responseData.toDataString());
return responseData;
}
3.4 小结
本节主要讲述了微信公众号发中常用的三个高级功能接口:微信网页授权,微信JS-SDK和图文消息交互。使用者三个功能及其扩展接口后,可以满足大部分的公众号和用户的交互需求,包括获取用户信息;调用操作系统接口如获取本地图片、获取地理位置和录音等等;与用户进行图文消息互动。