如今微信愈来愈火了,公众平台也愈来愈火,做为一个公司或者网站,没有一个公众号,你都很差意思跟人打招呼,更别提递名片了。python
可是,开通了公众平台,靠人工去响应用户消息,不但技术含量不够,并且,人少也忙不过来啊。编程
幸亏微信公众平台有开发模式,只要接入了微信后台,用户消息会被发送到咱们指定的服务器上,而后,由服务器向用户回复消息。这种方式,比提示用户“回复1看xxx,回复2看xxx”显得高端大气上档次。json
开发模式须要准备网站而且接入微信后台,在微信目前文档不完善,接口不友好的状况下,本文将详细讲解如何快速接入微信公众平台。ruby
准备工做
首先,你须要有一个微信公众号,好比“中华诗词”。在往下继续阅读前,请自觉掏出手机,打开微信扫一扫:服务器
其次,你须要有一个独立域名的网站,用来和微信服务器交互。微信
接入公众平台
登陆微信公众平台后台后,点“功能”-“高级功能”-“开发模式”,进入开发模式,若是公众平台显示“还没有成为开发者”,就点击“成为开发者”:微信公众平台
赞成协议后,填写URL和Token:框架
URL是指微信服务器向哪一个URL发送消息,假设咱们本身的服务器域名是www.example.com
,准备用/weixin
来接收消息,就填写:编程语言
http://www.example.com/weixin
而Token是微信服务器和咱们本身的服务器通讯时验证身份用的,能够随便填写,但要注意保密。
而后点“提交”,通常来讲会报错“URL超时”或者“没有正确返回echostr”,由于咱们的后台尚未准备好,因此,第一步是接收微信后台发送的验证消息,微信后台会发送一个GET请求到上面的URL,并附带如下参数:
signature,timestamp,nonce,echostr
咱们的服务器在接收到上述参数后,须要验证signature是否正确,验证方法是先对timestamp、nonce和token先排序,再拼接成一个字符串,计算出sha1,并和signature对比:
Python:
def check_signature(signature, timestamp, nonce): L = [timestamp, nonce, token] L.sort() s = L[0] + L[1] + L[2] return hashlib.sha1(s).hexdigest() == signature
Java:
public static boolean check_signature(signature, timestamp, nonce) { String[] arr = new String[] { timestamp, nonce, token }; Arrays.sort(arr); String s = arr[0] + arr[1] + arr[2]; md = MessageDigest.getInstance("SHA-1"); byte[] digest = md.digest(s.getBytes("utf-8")); return signature == bytes2HexString(digest);
注意token不是微信服务器发过来的,而是咱们本身写死的一个常量,就是在微信后台填写的Token。
若是计算的sha1和微信传过来的signature相等,说明这个请求确实是微信后台发过来的,若是是别人伪造的请求,因为他不知道token,因此,没法计算出正确的signature。
要防止第三方经过监听发动replay攻击,还须要验证timestamp和nonce,这个之后再讨论。
若是signature计算无误,就把微信后台传过来的echostr原封不动地传回去,这样,就能够经过验证,成为开发者。
在确保开发模式打开的状况下,微信后台会把用户消息发到咱们的服务器上,也就是URL:http://www.example.com/weixin
:
微信后台发送消息是一个POST请求,但和普通的POST请求不一样的是,首先,URL会带上signature、timestamp、nonce这3个参数:
POST http://www.example.com/weixin?signature=xxx×tamp=123456&nonce=123
而后,HTTP请求的BODY是一个不规范的XML:
<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>
咱们本身的服务器只须要处理该XML,而后,向微信返回一个相似以下的XML:
<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> </xml>
就能够完成消息的回复。微信后台要求必须在5秒内回复,最多重试3次,不然咱们本身的回复消息就到达不了用户的手机了。若是咱们本身的服务器没法在5秒内回复,就回复一个空字符串,告诉微信服务器,不用重试了,这个消息处理不了,不给用户回复了。
上面的交互逻辑看起来很简单,但实际上坑有不少。
首先,微信服务器发送的POST请求根本就不符合HTTP规范。原则上POST请求不该该在URL上附带参数,但微信后台恰恰要这么干,这就让不少编程语言的标准框架没法获取到POST参数,由于标准的POST参数是从HTTP BODY中解析的。
因此,从POST获取URL参数就须要用到更底层的代码。好比,在Python中,必须经过WSGI的environ字典获取,而且本身来解析:
# python: environ = ... qs = environ.get('query_string', '') q = urlparse.parse_qs(qs) signature = q['signature'][0] timestamp = q['timestamp'][0] nonce = q['nonce'][0] # TODO: check signature...
在Java中,用HttpServletRequest
在POST模式下别想用getParameter()
拿到URL参数,必须用getQueryString()
而后本身想办法解析字符串:
// java:
String qs = request.getQueryString();
Map<String, String> map = parse(qs); // TODO: check signature...
而后,咱们再讨论如何读取微信后台发过来的XML。在Python中,须要从environ
读取原始的wsgi.input
流:
fp = environ['wsgi.input']
在Java中,须要从HttpServletRequest
中获取Reader流:
Reader reader = request.getReader();
若是有乱码,写一个EncodingFilter把Request强制设置为UTF-8编码:
public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); chain.doFilter(req, resp); } public void init(FilterConfig config) throws ServletException {} public void destroy() {} }
不须要读取为字符串,只要有了流就能够解析XML了,建议用SAX解析,最终咱们应该获得微信的XML中传过来的几个值:
ToUserName: 'abc' FromUserName: 'xyz' CreateTime: '12345678' MsgType: 'text' Content: '用户发的消息'
根据MsgType咱们能够判断消息是文本、语音、图片、位置仍是视频,而后,构造一个XML回复给微信后台,若是一切顺利,微信后台就把咱们的消息发给用户。
目前咱们只讨论如何回复文本消息,只需构造以下的XML:
<xml> <ToUserName><![CDATA[xyz]]></ToUserName> <FromUserName><![CDATA[abc]]></FromUserName> <CreateTime>12345678</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[你好]]></Content> </xml>
在回复的XML中,把接收的ToUserName和FromUserName交换,这两个字符串都是用户ID(公众号自己也是一个用户ID),CreateTime是以秒为单位的UNIX时间戳,计算以下:
Python:
CreateTime = int(time.time())
Java:
long CreateTime = System.currentTimeMillis() / 1000;
MsgType还是text,Content就是咱们自动回复的消息,注意不要超过600个字符。
回复的时候,须要注意,一是最好明确地设置Content-Type: text/xml,二是XML的编码必须是UTF-8,不然,回复的消息就会出现乱码。
如何建立回复XML?因为该XML结构至关简单,因此无需动用任何XML接口,直接拼接字符串最简单快速。
最后,把代码部署到服务器,记住把接收的参数和XML,以及本身生成的XML在log中打印出来,一边看log,一边用手机端的微信来调试。只要调通了一种接口,其余接口参考微信文档就很容易开发了:
限制
目前,微信公众平台的API还有不少限制,好比没有天天自动群发消息的API,要回复图文等多媒体消息须要V认证等等。
思考
微信和微信公众平台虽然产品很先进,但后台API设计得确实不咋地。因为API是给开发人员使用的,因此,设计一个好的API要从开发人员的角度出发。与其使用笨重的XML,不如使用更符合Web潮流的JSON。并且,没有必要把验证单独用GET区分,彻底能够所有使用POST方式,在JSON中把全部信息所有包括,以action和data来区分消息类型和数据,例如,验证服务器:
{
"signature": "xxx", "timestamp": 123456, "nonce": "xxx", "action": "verify", "data": { "echostr": "echo" } }
发送消息:
{
"signature": "xxx", "timestamp": 123456, "nonce": "xxx", "action": "msg", "data": { "id": "123456", "type": "text", "from": "user-abc", "to": "user-xyz", "create_time": 1234567, "content": "blablabla..." } }
回复消息:
{
"action": "msg", "data": { "type": "text", "from": "user-xyz", "to": "user-abc", "create_time": 1234567, "content": "reply to..." } }
这样设计的API,各类编程语言都能处理,并且处理逻辑更简单,速度更快。