web.py+apache的微信公众号服务器
很久之前就注册了个微信公众号,可是一直都没有用起来。所以就想弄个服务器端实现些功能。最终的选择是用web.py加Apache虚拟机的方式部署在一个现有的外网服务器上。
微信公众号
首先你得有个注册公众号。注册地址https://mp.weixin.qq.com/。微信公众号分为“服务号”和“订阅号”。服务号功能更多。而且二者都有认证和非认证之分。认证的功能更多。所以说,非认证的个人订阅号功能最少。比如说微信不允许个人账号进行认证。 如果你的订阅号配置了服务器,就不能自定义菜单了。 (这里有个使用经验。配置服务器信息后,菜单就消失了。通过“添加功能插件” > “自定义菜单” > 开启。这样即使没有使用代码设置菜单,之前在微信公众平台设置的菜单还是能显示出来。)
微信公众号开发信息
登录微信公众平台 > 开发 > 基本配置中有我们需要或配置的一些信息:
- AppID(应用ID):系统给你的。
- AppSecret(应用密钥):后面生成access_token用。
- 服务器配置
- URL(服务器地址):服务器地址。http或https;域名或者外网地址;http必需是80端口,https必需是443端口。
- Token(令牌):Token可由开发者可以任意填写,用作生成签名。作用就是验证请求是不是来自微信。
- EncodingAESKey(消息加解密密钥:EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
- 消息加解密方式:明文模式、兼容模式和安全模式。模式的选择与服务器配置在提交后都会立即生效,请开发者谨慎填写及选择。加解密方式的默认状态为明文模式,选择兼容模式和安全模式需要提前配置好相关加解密代码。
微信公众号开发接入
配置服务器的时候,微信会向你配置的URL(服务器地址)发送一个GET请求,主要目的是验证签名。
包括以下参数:
参数 | 参数 |
---|---|
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
收到该请求后需要以下步骤完成接入验证:
- 将token、timestamp、nonce三个参数进行字典序排序
- 将三个参数字符串拼接成一个字符串进行sha1加密
- 开发者获得加密后的字符串可与signature对比,返回微信后台传过来的echostr
web.py处理该GET请求代码示例:
def GET(self):
data = web.input()
signature = data.signature
timestamp = data.timestamp
nonce = data.nonce
echostr = data.echostr
token = "xxxxxx"
list = [token, timestamp, nonce]
list.sort()
sha1 = hashlib.sha1()
map(sha1.update, list)
hashcode = sha1.hexdigest()
if hashcode == signature:
return echostr
接收消息和被动回复
关注者发到公众号的信息将通过POST请求发给你配置的URL(服务器地址)
该请求带有signature、timestamp、nonce这3个参数。
请求的数据为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如下:
def POST(self):
str_xml = web.data()
domTree = xml.dom.minidom.parseString(str_xml)
collection = domTree.documentElement
ToUserName = collection.getElementsByTagName('ToUserName')[0].childNodes[0].data
FromUserName = collection.getElementsByTagName('FromUserName')[0].childNodes[0].data
MsgType = collection.getElementsByTagName('MsgType')[0].childNodes[0].data
Content = collection.getElementsByTagName('Content')[0].childNodes[0].data
print ToUserName, FromUserName, MsgType, Content
我们的服务器解析该xml,然后通过下面格式的xml反给微信。
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>
响应中的ToUserName和FromUserName就是请求中的二者调换位置;CreateTime是以秒为单位的UNIX时间戳。
在web.py中使用模板的方法如下:
return render.wx(ToUserName, FromUserName, int(time.time()), answer)
对应模板文件templates\wx.xml
$def with (toUser,fromUser,createTime,content,funcFlag=0)
<xml>
<ToUserName><![CDATA[$fromUser]]></ToUserName>
<FromUserName><![CDATA[$toUser]]></FromUserName>
<CreateTime>$createTime</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[$content]]></Content>
<FuncFlag>$funcFlag</FuncFlag>
</xml>
订阅和取消订阅事件
订阅和取消订阅事件发生时微信发送事件推送,xml格式为:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe or unsubscribe]]></Event>
</xml>
订阅的时候要自动回复,返回和上面一样的xml就可以了:
$def with (toUser,fromUser,createTime,content,funcFlag=0)
<xml>
<ToUserName><![CDATA[$fromUser]]></ToUserName>
<FromUserName><![CDATA[$toUser]]></FromUserName>
<CreateTime>$createTime</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[$content]]></Content>
<FuncFlag>$funcFlag</FuncFlag>
</xml>
注意:返回的MsgType不要写成event
处理POST请求的代码:
MsgType = collection.getElementsByTagName('MsgType')[0].childNodes[0].data # event
try:
Event = collection.getElementsByTagName('Event')[0].childNodes[0].data
except IndexError:
pass
if MsgType == 'event':
if Event == 'subscribe':
answer = '欢迎关注!'
return render.wx(ToUserName, FromUserName, int(time.time()), answer)
调用公众号API
调用API,获取access token
接口为:
http请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
返回ACCESS_TOKEN及expires_in。expires_in为7200,表示2小时过期。因为每个接口调用有每天数限制。获取access token每天限制2000次。所以要保存获取的access token,在过期前再重新获取。
之后的所有API调用都要用到该ACCESS_TOKEN。
开发者工具:
- 在线接口调试工具:在线测试API及其应答。
- 公众平台测试帐号:测试用公众号。
web.py
安装
先安装python,然后用pip安装需要模块:
pip install web.python
pip install flup
部署
选择web.py是因为其轻。用到的功能:
-
路由
urls = ("/.*", "hello")
表示访问
/.*
由hello类处理。 -
处理GET和POST请求。在类中实现
GET(self)
和POST(self)
方法。代码如上。 -
模板,以下代码用来使用templates\wx.xml模版
render = web.template.render('templates/') return render.wx(ToUserName, FromUserName, int(time.time()), answer)
web.py + Apache
因为已有个外网服务器通过Apache虚拟主机来运行几个网站。下面的步骤就是添加一个新的虚拟主机作为我的微信服务器。
虚拟主机配置
apache主目录\conf\extra\httpd-vhosts.conf (apache 2.4)
<VirtualHost *:80>
AddHandler cgi-script .cgi .pl .py
ServerAdmin webmaster@dummy-host.example.com
DocumentRoot "d:/wamp/www/weixin/"
ServerName localhost
ServerAlias localhost
Alias /static "d:/wamp/www/weixin/static"
ScriptAlias / "d:/wamp/www/weixin/test.py"
<Directory />
Options +ExecCGI +FollowSymLinks -Indexes
Require all granted
</Directory>
# because Alias can be used to reference resources outside docroot, you
# must reference the directory with an absolute path
<Directory /static>
# directives to effect the static directory
Options +Indexes
</Directory>
</VirtualHost>
因为这个Apache来自wamp(一个Windows下的php+apache+mysql集成环境),已经使用CGI模块启用PHP。所以也用CGI来启动web.py。
遇到的2个坑:
-
Apache错误日志:
D:/wamp/www/weixin/test.py is not executable; ensure interpreted scripts have "#!" or "'!" first line
解决:必需在test.py第一行指定pyton地址。
#!C:\Python27\python.exe
-
例外
File "D:/wamp/www/weixin/test.py", line 18, in <module>\r app.run()\r File "C:\\Python27\\lib\\site-packages\\web\\application.py", line 313, in run\r return wsgi.runwsgi(self.wsgifunc(*middleware))\r File "C:\\Python27\\lib\\site-packages\\web\\wsgi.py", line 35, in runwsgi\r return runfcgi(func, None)\r File "C:\\Python27\\lib\\site-packages\\web\\wsgi.py", line 17, in runfcgi\r return flups.WSGIServer(func, multiplexed=True, bindAddress=addr, debug=False).run()\r File "C:\\Python27\\lib\\site-packages\\flup\\server\\fcgi.py", line 112, in run\r sock = self._setupSocket()\r File "C:\\Python27\\lib\\site-packages\\flup\\server\\fcgi_base.py", line 978, in _setupSocket\r sock = socket.fromfd(FCGI\_LISTENSOCK\_FILENO, socket.AF_INET,\r AttributeError: 'module' object has no attribute 'fromfd'\r
因为Windows中没有fromfd命令。解决方法:
注释Python27\lib\site-packages\flup\server\fcgi_base.py 978行左右的代码
sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET, socket.SOCK_STREAM) try: sock.getpeername() except socket.error, e: if e[0] == errno.ENOTSOCK: # Not a socket, assume CGI context. isFCGI = False elif e[0] != errno.ENOTCONN: raise 在其下添加: isFCGI = False
这样就部署完成了Windows + Apache + web.py环境作为微信服务器了。
github: https://github.com/snowyxx/WechatServer