微信公众平台部署手札 (I)

这段时间发现其实一味埋头做项目其实效率不高而且感觉没什么长进,因此还是一部分时间来做项目,一部分时间来追赶技术的第一集团军,这样感觉会比较好。

最近有研究 Linux,然后现在除了 Linux,日程上的还有 Git 和微信公众平台,周二看完了一本杂书《暗时间》,周三会计考试又通过了,因此昨天做了半天项目之后开启了微信平台的正式学习。

之前买了几本书,实在是浪费钱了,基本上复制粘贴官方文档,太无耻了。然后我在部署的过程也是一波三折,不过也居然一天解决了战斗,而且还有副产品,就是在 BAE3 上面学会了 Django 的部署!这个了不得,加上这个过程中间真的不少坑,因此综合起来得好好记录。

其实之前也草草做过一下微信平台的开发者验证,但是第一没有正式调试过接口,第二,用的是 PHP,既然现在已经用上了 Django,根据要走窄门的原则,还是按照难的来搞。这次还是用《乐德学堂》的公众号来搞。


1. 项目的创建(验证准备)

之前也验证过,不同的是这次用 Django,上段时间买的 Linux vps 这次正式派上用场了,按照此前的老路(看本博客前面的 Linux 部署手札)将 Django 部署好,然后就开始对公众平台进行验证(因为每修改一次 url 和 token 都要验证一下)

验证的过程可概括为:接收一个 GET 请求,然后处理,把结果直接文本返回 HttpResponse。

(前面假设我们的 django 环境已经部署好了)

好了,现在先创建一个项目:

mkdir /var/django cd /var/django python3 /usr/local/bin/django-admin.py startproject mysite cd mysite python3 manage.py startapp myapp

如是,生成好 django 的项目,在里面建立一个 app。

然后呢,我们配置好路由,直接用根目录就好了,按照之前部署 linux 的步骤,我们将 apache2 的根目录解析到我们的这个 django 项目:

# /etc/apache2/sites-available/000-default.conf

在最后一个 前面加上这段:

WSGIScriptAlias / /var/django/mysite/mysite/wsgi.py Order deny,allow Require all granted

# /var/django/mysite/mysite/wsgi.py

import sys sys.path.append('/var/django/mysite') sys.path.append('/var/django/mysite/mysite')

上面这段记得加,后面用 BAE 的过程,这段代码的一些调整让我更了解 django 项目启动的过程,现在先这样搞。

然后配置 django 路由,反正我们将网址根目录绑定到一个视图先:

# /var/django/mysite/mysite/urls.py

# 加上这条
url(r'^', include('myapp.urls', namespace='myapp')),

我们要将路由做成一个模块,因此这样麻烦一点,在 myapp 这个 app 里面再创建一个 urls.py,然后让 mysite 站的 urls.py include 这个路由。

# /var/django/mysite/myapp/urls.py

from django.conf.urls import patterns, include, url

urlpatterns = patterns('myapp.views', url(r'^$', 'validate', name='index'), )

如是,现在通过访问根目录(我整了个域名 wx.easecloud.cn 指定到我的 vps,搞完之后直接访问这个地址会去找 myapp.views.validate 这个视图来处理请求。)

当然,其实这里还有一步不能少的就是应该在 settings.py 里面的 INSTALLED_APPS 里面加上 ‘myapp’

然后呢,我们来写这个验证的视图。

2. 平台验证

于是,回到公众平台《开发模式》,修改一下服务器配置,我这里是这样子:

url: http://wx.easecloud.cn token: 12345678

提交的时候就要求这个地址能够正确响应验证请求了,看一下文档,我们来分析一下:

反正我们要通过三个字符串(timestamp/token/nonce)折腾出一个 SHA1 串(其中 timestamp 和 nonce 是 get 请求过来的,token 使我们设置的 12345678),与 signature(也是 get 过来的)比较,如果相同输出同样是 get 来的 echostr,否则输出空白。

“折腾”的过程是将三个串排序拼接再 SHA1,这段代码卸载 views.py 里面:

# /var/django/mysite/myapp/views.py

from django.shortcuts import render from django.http import HttpResponse from django.template import RequestContext from hashlib import sha1

def validate(request, *args, **kwargs): signature = request.GET.get('signature', '') timestamp = request.GET.get('timestamp', '') nonce = request.GET.get('nonce', '') echostr = request.GET.get('echostr', '') token = '12345678' raw = ''.join(sorted([token, timestamp, nonce])).encode('utf8') key = sha1(raw).hexdigest() if key.upper() != signature.upper(): echostr = '' return HttpResponse(echostr)

下面分析一下代码:

首先,我们安全地(用 request.GET.get(key, ”),而不是直接用[]取值)获取参数 signature/timestamp/nonce/echostr,硬编码一下 token,然后我们拼接(python 爽吧)成 raw 这个字符串。。。

唉,写着写着发现自己太啰嗦了,这么简单的代码不需要解释了。。

然后测试一下,验证就通过了,反正注意一下 sha1 的计算方法,在调用之前必须 encode 成 utf8 一下,以确保 sha1 获取的参数是一个 raw string。

3. 接收消息并回复

事实上未经认证的订阅号接口功能是很受限的,很多功能的实现是依赖于超链接到其他网页完成的,我们可以做的实际上只有:

在用户有向订阅号发送消息的情况下,回复一条消息,当然,双向的消息可以是各种类型的,包括声音图片视频地理位置等。

也就是说,发送消息只能是“回复”,而不能够主动推送,如果一定要的话要花 300 块买一个 access_token 或者升级成服务号。

那么如果是回复,业务流程如下:

  1. 订阅用户发消息给公众号
  2. 微信服务器将用户发的消息打包成一个 xml 对象 POST 给你的服务器
  3. 服务器处理数据并将回复内容打包成 xml 对象 response 回去给微信服务器
  4. 微信服务器解析 response 的消息并发回给订阅用户

也就是说,我们依然要用这个 url 接收来自微信服务器的 post 请求,然后按照逻辑返回相应的 response。

4. 简单回复的实现

先参看文档(接收消息格式/回复消息格式),我们先做一个简单的,将请求过来的内容直接回复过去(用户发什么内容,就回复什么内容)。

那么现在我们改一下路由,让其解析到一个新的视图 response_msg:

# /var/django/mysite/myapp/urls.py

...

改一下这条,指定到新的视图

url(r'^$', 'response_msg', name='index'),

...

然后创建一个新的视图

# /var/django/mysite/myapp/views.py

import xmltodict

def response_msg(request, *args, **kwargs): wxreq = xmltodict.parse(request.body).get('xml') res = { 'xml': { 'ToUserName': wxreq.get('FromUserName'), 'FromUserName': wxreq.get('ToUserName'), 'CreateTime': wxreq.get('CreateTime'), 'MsgType': 'text', 'Content': wxreq.get('Content'), 'MsgId': wxreq.get('MsgId'), } } return HttpResponse(xmltodict.unparse(res))

先直接上这段最终的代码,下面有一些要点和问题:

a. xml 格式的处理

xml 处理起来貌似不是很方便,但是找到了一个不错的库 xmltodict,可以支持从 xml 字符串和 python 字典之间的相互转换。

使用 pip3 install xmltodict 即可安装好,通过 request.body (Django 1.6 之前是 request.raw_post_data,但在 1.6 被正式弃用)可以获取从微信服务器发来的 xml 请求内容。

通过 xmltodict.parse 和 xmltodict.unparse 来解码和编码 xml 字符串;

至于内容本身,请求方面给出了 FromUserName, ToUserName, CreateTime 等等,返回的时候记得把 From 和 To 倒过来,才可以回复得到。

b. 在调试工具测试接口

打开接口调试工具,可以用来向服务端模拟发送消息。

填写好表单之后点《检查问题》,就可以看到这样的表单看到返回的 HttpResponse 内容。

当时为了调试方便,我还把 request 来的 post 数据存到 log 里面来分析,在视图里面在家这一句,然后可以从日志文件监测 request 内容:

log = open('/var/django/django.log', 'a+') log.writelines([request.body]) log.close()

不过现在这样应该是没有反应的,差了下面这一步。

c. 关闭 csrf 跨站请求过滤

由于 django 默认对 post 来的数据会校验 csrf_token,也就是说平时提交的 post 表单必须验证一个动态的 post 字段,叫做 csrf_token,如果校验不符,即被认为是跨站请求,会被拒绝,但是现在微信这样子搞法看样子就必须是跨站的,因此我们要想办法将其关掉。

上网稍为找了一下办法,需要用一个中间件来拦截 csrf 的启用:

在 app 里面添加一个 middleware.py:

# /var/django/mysite/myapp/middleware.py class CsrfFuckMiddleware(object): def process_view(self, request, callback, callback_args, callback_kwargs): setattr(request, '_dont_enforce_csrf_checks', True)

然后在 settings.py 里面,在 MIDDLEWARE_CLASSES 配置中,CsrfViewMiddleware 前面加上这个 middleware 的引用,大概是这个样子:

# /var/django/mysite/mysite/settings.py MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'myapp.middleware.CsrfFuckMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', )

重启一下 apache2,然后应该就可以返回 200 正常的结果了,没有这个的话调试工具是会得到一个 500 响应,那就完全没有反应的了。

5. 应该是好了,但是。。

但是,我发现用户发消息给公众号的时候并没有得到回复,经过细心反复调试,也都不行,并且发现其实服务是没有接收到 http request 的!

然后后来加了个群才有人说:国外服务器不行!

Bingo! Sentenced to DEATH…

不过最终我还是搞定了,请看下一篇。。我们用 BAE 来干这个活!


【转载请附】愿以此功德,回向 >>

原文链接:https://www.huangwenchao.com.cn/2014/04/wx-1.html【微信公众平台部署手札 (I)】

《微信公众平台部署手札 (I)》有1个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注