前言

最近闲的蛋疼想抛弃微信官方提供的服务,想把公众号服务都迁移到云服务器上,却发现接口权限太少了…就把瞎捣鼓过程记录一下。

一、基本配置

主要是添加urltoken,这里url必须以http://https://开头,分别支持80端口和443端口。所以服务器那边就用Nginx监听443端口然后转发到给微信api分配的端口(本文是8000)。

微信公众号 网页授权 java后台 微信公众号接口权限_微信

二、服务器身份验证

自己的服务器接收到GET请求时,需要校验消息是不是微信的服务器发过来的,代码如下。

@app.route('/', methods=['GET', 'POST'])
def wechat():
    args = request.args
    signature = args['signature']
    timestamp = args['timestamp']
    nonce = args['nonce']
    token = 'xxxxxxxxx'
    
    if not all([signature, timestamp, nonce, echostr]):
        abort(400)
    
    # 根据token, timestamp, nonce构造hashcode
    list = sorted([token, timestamp, nonce])
    s = list[0]+list[1]+list[2]
    hashcode = hashlib.sha1(s.encode('utf-8')).hexdigest()

    if request.method=='GET':
        # GET仅用于配置服务器时校验 检查hashcode是否等于signature
        echostr = args['echostr']
        if hashcode == signature:
            return echostr
        else:
            return "Sign error."

三、处理用户消息

用户发给微信服务器,微信服务器给搞成XML发给自己的服务器,类似下面这样。其中MsgType是消息类型,比如文本、图片、小程序、视频、语音等。分别根据MsgType来处理和回复。

<xml>
 <ToUserName><![CDATA[公众号]]></ToUserName>
 <FromUserName><![CDATA[粉丝号]]></FromUserName>
 <CreateTime>1460537339</CreateTime>
 <MsgType><![CDATA[text]]></MsgType>
 <Content><![CDATA[木尧大兄弟nb]]></Content>
 <MsgId>3272960105994287638</MsgId>
</xml>

接收到xml后,我用xmltodict.parse方法将xml转成类似字典进行后续处理,最后用xmltodict.unparse方法将dict再转成xml返回给微信服务器,微信服务器再发给用户。

完整代码如下:

from flask import Flask, jsonify, request, Response, abort
from gevent import pywsgi
from flask_cors import CORS
import time
import hashlib
import xmltodict

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False  # 支持中文
app.config['JSON_SORT_KEYS'] = False  # 禁止json自动排序
CORS(app, supports_credentials=True)  # 允许跨域


@app.route('/', methods=['GET', 'POST']) #, strict_slashes=False)
def wechat():
    args = request.args
    signature = args['signature']
    timestamp = args['timestamp']
    nonce = args['nonce']
    token = 'xxxxxxxxxxxx'
    
    if not all([signature, timestamp, nonce]):
        abort(400)
    
    list = sorted([token, timestamp, nonce])
    s = list[0]+list[1]+list[2]
    hashcode = hashlib.sha1(s.encode('utf-8')).hexdigest()

    if request.method=='GET':
        # 仅用于配置服务器时校验
        echostr = args['echostr']
        if hashcode == signature:
            return echostr
        else:
            return "Sign error."
    
    if request.method=='POST':
    
        # 监测是否是微信发来的请求
        if hashcode != signature:
            abort(403)
        
        xml = request.data
        if not xml:
            abort(400)
        
        # parse:xml -> dict
        req = xmltodict.parse(xml)['xml']
        print(req)
        
        
        # 用户发来的是文字
        if 'text' == req.get('MsgType'):
            resp = {
                'ToUserName':req.get('FromUserName'),
                'FromUserName':req.get('ToUserName'),
                'CreateTime':int(time.time()),
                'MsgType':'text',
                'Content':req.get('Content')
            }
        
        # 用户发来的是语音
        elif 'voice' == req.get('MsgType'):
            reg = req.get('Recognition')
            resp = {
                'ToUserName':req.get('FromUserName'),
                'FromUserName':req.get('ToUserName'),
                'CreateTime':int(time.time()),
                'MsgType':'text',
                'Content':reg if reg else '你说的嘛呀!听不懂'
            }
        
        # 用户发来的是图片        
        elif 'image' == req.get('MsgType'):
            resp = {
                'ToUserName': req.get('FromUserName'),
                'FromUserName': req.get('ToUserName'),
                'CreateTime': int(time.time()),
                'MsgType': 'text',
                'Content': '好图,好图!'
            }
        
        # 用户发来的是位置        
        elif 'location' == req.get('MsgType'):
            resp = {
                'ToUserName': req.get('FromUserName'),
                'FromUserName': req.get('ToUserName'),
                'CreateTime': int(time.time()),
                'MsgType': 'text',
                'Content': f'原来你在位于(X, Y)=({req.get("Location_X")}, {req.get("Location_Y")})的{req.get("Label")}!'
            }
        
        # 用户发来的是视频     
        elif 'video' == req.get('MsgType'):
            resp = {
                'ToUserName': req.get('FromUserName'),
                'FromUserName': req.get('ToUserName'),
                'CreateTime': int(time.time()),
                'MsgType': 'text',
                'Content': '不要发些奇奇怪怪的东西嗷'
            }
        
        # 用户发来的是event(关注/取关)
        elif 'event' == req.get('MsgType'):
            if "subscribe" == req.get("Event"):
                resp = {
                    "ToUserName":req.get("FromUserName", ""),
                    "FromUserName":req.get("ToUserName", ""),
                    "CreateTime":int(time.time()),
                    "MsgType":"text",
                    "Content":u"感谢您的关注!"
                }
            else:
                print('取关!')
                resp = None
            
        # 用户发来的是其他东西 随便返回个字符串 要不然用户那边显示服务出问题了
        else:
            return "success"
        
        # unparse:dict -> xml
        xml = xmltodict.unparse({'xml':resp if resp else ''})
        return xml

if __name__ == '__main__':
    server = pywsgi.WSGIServer(('0.0.0.0', 8000), app)
    server.serve_forever()

补充:微信内置网页的授权

1、公众号权限设置那里,设置网页授权回调的允许域名
2、用户同意授权,获取code(用户访问如下网址,REDIRECT_URI就是点完授权要跳转到的URL,跳转时会带着code参数)

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

微信公众号 网页授权 java后台 微信公众号接口权限_xml_02


3、通过第2步拿到的code换取网页授权access_token。方法是在自己服务端发GET请求如下:

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

微信公众号 网页授权 java后台 微信公众号接口权限_微信_03


返回结果如下:

{
   "access_token":"ACCESS_TOKEN",
   "expires_in":7200,
   "refresh_token":"REFRESH_TOKEN",
   "openid":"OPENID",
   "scope":"SCOPE"
}

4、拿着ACCESS_TOKENOPENID换用户基本信息,也是GET请求:

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

微信公众号 网页授权 java后台 微信公众号接口权限_xml_04


返回的json结果包含如下字段,然后拿去前端展示就完事儿了:

微信公众号 网页授权 java后台 微信公众号接口权限_xml_05