微信服務(wù)號(hào)開發(fā)

微信服務(wù)號(hào)開發(fā)

整體流程

  1. 域名報(bào)備稚配,服務(wù)器搭建
  2. Python開發(fā)環(huán)境和項(xiàng)目的初始化搭建体箕;
  3. 微信公眾號(hào)注冊(cè)及開發(fā)模式校驗(yàn)配置;
  4. 接收關(guān)注/取關(guān)事件推送和自動(dòng)回復(fù)图柏;
  5. IOLoop定時(shí)獲取access_token和jsapi_ticket;
  6. 自定義菜單及點(diǎn)擊時(shí)獲取openid及用戶信息任连;
  7. 菜單中網(wǎng)頁的開發(fā), JS-SDK的使用蚤吹;
  8. 完成測(cè)試,發(fā)布上線,部署至服務(wù)器。

思維導(dǎo)圖:

思維導(dǎo)圖
思維導(dǎo)圖

一.域名報(bào)備随抠,服務(wù)器搭建

微信要求的域名是備案過的域名裁着,且要下載微信提供的檢測(cè)文件,放在搭建的服務(wù)器下拱她,然后對(duì)域名進(jìn)行測(cè)試二驰,通過才能添加。

業(yè)務(wù)域名
業(yè)務(wù)域名

網(wǎng)頁授權(quán)域名
網(wǎng)頁授權(quán)域名

js接口安全域名
js接口安全域名

搭建項(xiàng)目服務(wù)器 Nginx+Tornado+Supervisor 部署操作方法

搭建Redis服務(wù)器 進(jìn)行數(shù)據(jù)存儲(chǔ) Redis安裝方法

二.Python開發(fā)環(huán)境搭建(Linux系統(tǒng))及項(xiàng)目初始化搭建

  1. 安裝python及pip,并配置環(huán)境變量,安裝tornado框架
    (1) 下載Python-3.5.2包并安裝 https://www.python.org/downloads
    (2) 將python配置到系統(tǒng)環(huán)境變量
    (3) 下載pip包并安裝 https://pypi.python.org/pypi/pip#downloads
    (4) 將pip配置到系統(tǒng)環(huán)境變量
    (5) 使用pip安裝tornado框架

     pip install tornado
    
  2. 創(chuàng)建微信服務(wù)號(hào)的后臺(tái)項(xiàng)目椭懊,使用Tornado搭建項(xiàng)目入口,端口號(hào)為8000
    微信服務(wù)端校驗(yàn)的接口文件 wxauthorize.py

     import hashlib
     import tornado.web
     from core.logger_helper import logger  
    
     class WxSignatureHandler(tornado.web.RequestHandler):
         """
         微信服務(wù)器簽名驗(yàn)證, 消息回復(fù)
         
         check_signature: 校驗(yàn)signature是否正確
         """
         
         def data_received(self, chunk):
             pass
         
         def get(self):
             try:
                 signature = self.get_argument('signature')
                 timestamp = self.get_argument('timestamp')
                 nonce = self.get_argument('nonce')
                 echostr = self.get_argument('echostr')
                 logger.debug('微信sign校驗(yàn),signature='+signature+',&timestamp='+timestamp+'&nonce='+nonce+'&echostr='+echostr)
                 result = self.check_signature(signature, timestamp, nonce)
                 if result:
                     logger.debug('微信sign校驗(yàn),返回echostr='+echostr)
                     self.write(echostr)
                 else:
                     logger.error('微信sign校驗(yàn),---校驗(yàn)失敗')
             except Exception as e:
                 logger.error('微信sign校驗(yàn),---Exception' + str(e))
         
         def check_signature(self, signature, timestamp, nonce):
             """校驗(yàn)token是否正確"""
             token = 'yzgtest123456'
             L = [timestamp, nonce, token]
             L.sort()
             s = L[0] + L[1] + L[2]
             sha1 = hashlib.sha1(s.encode('utf-8')).hexdigest()
             logger.debug('sha1=' + sha1 + '&signature=' + signature)
             return sha1 == signature
    

配置Tornado的url路由規(guī)則 url.py

    from core.server.wxauthorize import WxSignatureHandler
    import tornado.web
    '''web解析規(guī)則'''
    
    urlpatterns = [
        (r'/weixin', WxSignatureHandler),  # 微信簽名
   ]

基本配置文件 run.py

import os
import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options
from core.url import urlpatterns

define('port', default=8000, help='run on the given port', type=int)

class Application(tornado.web.Application):
    def __init__(self):
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "core/template"),
            static_path=os.path.join(os.path.dirname(__file__), "core/static"),
            debug=True,
            login_url='/login',
            cookie_secret='MuG7xxacQdGPR7Svny1OfY6AymHPb0H/t02+I8rIHHE=',
        )
        super(Application, self).__init__(urlpatterns, **settings)


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':
    main()

三. 微信公眾號(hào)注冊(cè)及開發(fā)模式校驗(yàn)配置

  1. 微信公眾號(hào)注冊(cè)
    官網(wǎng)鏈接https://mp.weixin.qq.com,需要注冊(cè)為服務(wù)號(hào)诸蚕,依次填寫信息進(jìn)行注冊(cè)
  2. 微信公眾開發(fā)模式校驗(yàn)配置
    (1)登錄微信公眾號(hào)后, 進(jìn)入基本配置,如下:
服務(wù)器配置
服務(wù)器配置

url填寫為服務(wù)器域名+我們項(xiàng)目中微信校驗(yàn)的接口名
token 填寫為我們項(xiàng)目中自定義的token: yzgtest123456
EncodingAESKey 點(diǎn)擊"隨機(jī)生成"按鈕即可,消息加密方式使用明文模式

填寫完畢后,先啟動(dòng)我們的項(xiàng)目,運(yùn)行python run.py指令后, 保證我們的服務(wù)器是運(yùn)行著的, 然后點(diǎn)擊"提交",如果你是按照以上流程操作的話,會(huì)提示提交成功,否則校驗(yàn)失敗,需要我們通過日志檢查是哪一塊出了問題.
(2) 接下來,校驗(yàn)成功后,點(diǎn)擊啟用,即可激活開發(fā)者模式

四. 接收關(guān)注/取關(guān)事件推送和自動(dòng)回復(fù)步势;

  1. 接收關(guān)注/取關(guān)事件推送
    在開發(fā)模式中,有新用戶關(guān)注我們的公眾號(hào)時(shí),微信公眾平臺(tái)會(huì)使用http協(xié)議的Post方式推送數(shù)據(jù)至我們的后臺(tái)微信校驗(yàn)的接口,在接收到消息后,我們后臺(tái)發(fā)送一條歡迎語給該用戶,關(guān)于微信公眾平臺(tái)推送消息的具體內(nèi)容和數(shù)據(jù)格式,詳見微信開發(fā)文檔

以下是在wxauthorize.py文件中增加的post方法,用來接收事件推送

def post(self):
    body = self.request.body
    logger.debug('微信消息回復(fù)中心】收到用戶消息' + str(body.decode('utf-8')))
    data = ET.fromstring(body)
    ToUserName = data.find('ToUserName').text
    FromUserName = data.find('FromUserName').text
    MsgType = data.find('MsgType').text
    if MsgType == 'event':
        '''接收事件推送'''
        try:
            Event = data.find('Event').text
            if Event == 'subscribe':
                # 關(guān)注事件
                CreateTime = int(time.time())
                reply_content = '歡迎關(guān)注我的公眾號(hào)~'
                out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                self.write(out)
        except:
            pass

def reply_text(self, FromUserName, ToUserName, CreateTime, Content):
    """回復(fù)文本消息模板"""
    textTpl = """<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content></xml>"""
    out = textTpl % (FromUserName, ToUserName, CreateTime, 'text', Content)
    return out

2.自動(dòng)回復(fù)

(1) 同接收關(guān)注/取關(guān)事件推送消息一樣,用戶給我們公眾號(hào)發(fā)送消息時(shí),微信公眾平臺(tái)也會(huì)推送數(shù)據(jù)至我們的后臺(tái)微信校驗(yàn)的接口,在接收到消息后,我們?nèi)〕鲎远x的關(guān)鍵字進(jìn)行匹配,匹配到了就執(zhí)行自動(dòng)回復(fù)
(2) 微信公眾平臺(tái)也提供了語音識(shí)別功能, 將用戶發(fā)送的語音內(nèi)容識(shí)別轉(zhuǎn)化為文字,發(fā)送給我們后臺(tái),在使用該功能時(shí)需要在接口權(quán)限中打開語音識(shí)別功能.

以下是在wxauthorize.py中的post方法中增加的一個(gè)判斷,用來匹配用戶文本消息和語音消息中的關(guān)鍵字

def post(self):
    body = self.request.body
    logger.debug('微信消息回復(fù)中心】收到用戶消息' + str(body.decode('utf-8')))
    data = ET.fromstring(body)
    ToUserName = data.find('ToUserName').text
    FromUserName = data.find('FromUserName').text
    MsgType = data.find('MsgType').text
    if MsgType == 'text' or MsgType == 'voice':
        '''文本消息 or 語音消息'''
        try:
            MsgId = data.find("MsgId").text
            if MsgType == 'text':
                Content = data.find('Content').text  # 文本消息內(nèi)容
            elif MsgType == 'voice':
                Content = data.find('Recognition').text  # 語音識(shí)別結(jié)果氧猬,UTF8編碼
            if Content == u'你好':
                reply_content = '您好,請(qǐng)問有什么可以幫助您的嗎?'
            else:
                # 查找不到關(guān)鍵字,默認(rèn)回復(fù)
                reply_content = "客服小鋼智商不夠用啦~"
            if reply_content:
                CreateTime = int(time.time())
                out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                self.write(out)
        except:
            pass

    elif MsgType == 'event':
        '''接收事件推送'''
        try:
            Event = data.find('Event').text
            if Event == 'subscribe':
                # 關(guān)注事件
                CreateTime = int(time.time())
                reply_content = self.sys_order_reply
                out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
                self.write(out)
        except:
            pass

def reply_text(self, FromUserName, ToUserName, CreateTime, Content):
    """回復(fù)文本消息模板"""
    textTpl = """<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content></xml>"""
    out = textTpl % (FromUserName, ToUserName, CreateTime, 'text', Content)
    return out

五.IOLoop定時(shí)獲取access_token和jsapi_ticket

  1. access_token
    access_token是公眾號(hào)的全局唯一票據(jù),公眾號(hào)調(diào)用各接口時(shí)都需使用坏瘩。
    access_token的有效期為7200秒,開發(fā)者需要進(jìn)行妥善保存盅抚。access_token的存儲(chǔ)至少要保留512個(gè)字符空間。access_token的有效期目前為2個(gè)小時(shí)倔矾,需定時(shí)刷新妄均,重復(fù)獲取將導(dǎo)致上次獲取的access_token失效。詳見微信開發(fā)文檔
  2. jsapi_ticket
    jsapi_ticket是公眾號(hào)用于調(diào)用微信JS接口的臨時(shí)票據(jù)哪自。
    正常情況下丰包,jsapi_ticket的有效期為7200秒,通過access_token來獲取壤巷。由于獲取jsapi_ticket的api調(diào)用次數(shù)非常有限邑彪,頻繁刷新jsapi_ticket會(huì)導(dǎo)致api調(diào)用受限,影響自身業(yè)務(wù)胧华,開發(fā)者必須在自己的服務(wù)全局緩存jsapi_ticket 寄症。參考文檔JS-SDK使用權(quán)限簽名算法
  3. redis數(shù)據(jù)庫
    用來存儲(chǔ)access_token和jsapi_ticket等,方便快捷矩动。Redis快速入門
    配置redis程序:
    basecache.py
    import redis
    
    """緩存服務(wù)器"""
    CACHE_SERVER = {
        'host': '127.0.0.1',
        'port': 6379,
        'database': 0,
        'password': '',
    }

    class BaseCache(object):
    """
    緩存類父類
    redis_ctl:                          redis控制句柄
    """
        _host = CACHE_SERVER.get('host', '')
        _port = CACHE_SERVER.get('port', '')
        _database = CACHE_SERVER.get('database', '')
        _password = CACHE_SERVER.get('password', '')
    
        @property
        def redis_ctl(self):
            """redis控制句柄"""
            redis_ctl = redis.Redis(host=self._host, port=self._port, db=self._database, password=self._password)
            return redis_ctl  

tokencache.py

from core.cache.basecache import BaseCache
from core.logger_helper import logger


class TokenCache(BaseCache):
    """
    微信token緩存

    set_cache               添加redis
    get_cache               獲取redis
    """
    _expire_access_token = 7200  # 微信access_token過期時(shí)間, 2小時(shí)
    _expire_js_token = 30 * 24 * 3600   # 微信js網(wǎng)頁授權(quán)過期時(shí)間, 30天
    KEY_ACCESS_TOKEN = 'access_token'  # 微信全局唯一票據(jù)access_token
    KEY_JSAPI_TICKET = 'jsapi_ticket'  # JS_SDK權(quán)限簽名的jsapi_ticket

    def set_access_cache(self, key, value):
        """添加微信access_token驗(yàn)證相關(guān)redis"""
        res = self.redis_ctl.set(key, value)
        self.redis_ctl.expire(key, self._expire_access_token)
        logger.debug('【微信token緩存】setCache>>>key[' + key + '],value[' + value + ']')
        return res

    def set_js_cache(self, key, value):
        """添加網(wǎng)頁授權(quán)相關(guān)redis"""
        res = self.redis_ctl.set(key, value)
        self.redis_ctl.expire(key, self._expire_js_token)
        logger.debug('【微信token緩存】setCache>>>key[' + key + '],value[' + value + ']')
        return res

    def get_cache(self, key):
        """獲取redis"""
        try:
            v = (self.redis_ctl.get(key)).decode('utf-8')
            logger.debug(v)
            logger.debug('【微信token緩存】getCache>>>key[' + key + '],value[' + v + ']')
            return v
        except Exception:
            return None             

4有巧。 使用tornado的 Ioloop 實(shí)現(xiàn)定時(shí)獲取access_token和 jsapi_ticket,并將獲取到的access_token和 jsapi_ticket保存在Redis數(shù)據(jù)庫中
wxconfig.py

class WxConfig(object):
    """
    微信開發(fā)--基礎(chǔ)配置

    """
    AppID = 'wxxxxxxxxxxxxxxxx'  # AppID(應(yīng)用ID)
    AppSecret = 'xxxxxxxxxxxxxxxxxxxxxxx'  # AppSecret(應(yīng)用密鑰)

    '''獲取access_token'''
    config_get_access_token_url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s' % (AppID, AppSecret)

wxshedule.py

from core.logger_helper import logger
import tornado.ioloop
import requests
import json
from core.server.wxconfig import WxConfig
from core.cache.tokencache import TokenCache


class WxShedule(object):
    """
    定時(shí)任務(wù)調(diào)度器

    excute                      執(zhí)行定時(shí)器任務(wù)
    get_access_token            獲取微信全局唯一票據(jù)access_token
    get_jsapi_ticket           獲取JS_SDK權(quán)限簽名的jsapi_ticket
    """
    _token_cache = TokenCache()  # 微信token緩存實(shí)例
    _expire_time_access_token = 7000 * 1000  # token過期時(shí)間

    def excute(self):
        """執(zhí)行定時(shí)器任務(wù)"""
        logger.info('【獲取微信全局唯一票據(jù)access_token】>>>執(zhí)行定時(shí)器任務(wù)')
        tornado.ioloop.IOLoop.instance().call_later(0, self.get_access_token)
        tornado.ioloop.PeriodicCallback(self.get_access_token, self._expire_time_access_token).start()
        # tornado.ioloop.IOLoop.current().start()
        
    def get_jsapi_ticket(self):
        """獲取JS_SDK權(quán)限簽名的jsapi_ticket"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi' % access_token
            r = requests.get(url)
            logger.info('【微信JS-SDK】獲取JS_SDK權(quán)限簽名的jsapi_ticket的Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.info('【微信JS-SDK】獲取JS_SDK權(quán)限簽名的jsapi_ticket>>>>' + res)
                d = json.loads(res)
                errcode = d['errcode']
                if errcode == 0:
                    jsapi_ticket = d['ticket']
                    # 添加至redis中
                    self._token_cache.set_access_cache(self._token_cache.KEY_JSAPI_TICKET, jsapi_ticket)
                else:
                    logger.info('【微信JS-SDK】獲取JS_SDK權(quán)限簽名的jsapi_ticket>>>>errcode[' + errcode + ']')
                    logger.info('【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s')
                    tornado.ioloop.IOLoop.instance().call_later(10, self.get_jsapi_ticket)
            else:
                logger.info('【微信JS-SDK】request jsapi_ticket error, will retry get_jsapi_ticket() method after 10s')
                tornado.ioloop.IOLoop.instance().call_later(10, self.get_jsapi_ticket)
        else:
            logger.error('【微信JS-SDK】獲取JS_SDK權(quán)限簽名的jsapi_ticket時(shí),access_token獲取失敗, will retry get_access_token() method after 10s')
            tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)

if __name__ == '__main__':

    wx_shedule = WxShedule()
    """執(zhí)行定時(shí)器"""
    wx_shedule.excute()

run.py
將定時(shí)器的啟動(dòng)放在主程序入口處,保證每次啟動(dòng)服務(wù)器時(shí),重新啟動(dòng)定時(shí)器

import os
import tornado.httpserver
import tornado.ioloop
import tornado.web
from tornado.options import define, options
from core.url import urlpatterns
from core.server.wxshedule import WxShedule

define('port', default=8000, help='run on the given port', type=int)


class Application(tornado.web.Application):
    def __init__(self):
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "core/template"),
            static_path=os.path.join(os.path.dirname(__file__), "core/static"),
            debug=True,
            login_url='/login',
            cookie_secret='MuG7xxacQdGPR7Svny1OfY6AymHPb0H/t02+I8rIHHE=',
        )
        super(Application, self).__init__(urlpatterns, **settings)


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    # 執(zhí)行定時(shí)任務(wù)
    wx_shedule = WxShedule()
    wx_shedule.excute()
    tornado.ioloop.IOLoop.current().start()

if __name__ == '__main__':

六. 自定義菜單及點(diǎn)擊菜單時(shí)獲取openid

  1. 編寫菜單對(duì)應(yīng)的html頁面

(1)先在template模板文件夾下制作一個(gè)index.html頁面,用于點(diǎn)擊自定義菜單時(shí)跳轉(zhuǎn)到的網(wǎng)頁.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    這是一個(gè)測(cè)試頁面
    </body>
    </html> 

(2) 編寫一個(gè)頁面處理類,用于接收tornado.web.RequestHandler請(qǐng)求

    import tornado.web

    class PageHandler(tornado.web.RequestHandler):
    """
    微信handler處理類
    """
    def post(self, flag):
        if flag == 'index':
        '''首頁'''
        self.render('index.html')

(3) 給PageHandler添加url規(guī)則,在url.py中添加

    from core.server.wxauthorize import WxSignatureHandler
    from core.server.page_handler import PageHandler
    from core.server.wx_handler import WxHandler


    '''web解析規(guī)則'''

    urlpatterns = [
        (r'/weixin', WxSignatureHandler),  # 微信簽名
        (r'/page/(.*)', PageHandler),  # 加載頁面
        (r'/wx/(.*)', WxHandler),  # 網(wǎng)頁授權(quán)
    ] 
  1. 創(chuàng)建一個(gè)菜單,并給菜單添加獲取授權(quán)code的url

以下是微信公眾平臺(tái)官方文檔給出的具體流程:詳見網(wǎng)頁授權(quán)獲取用戶基本信息

關(guān)于網(wǎng)頁授權(quán)回調(diào)域名的說明:

1悲没、在微信公眾號(hào)請(qǐng)求用戶網(wǎng)頁授權(quán)之前篮迎,開發(fā)者需要先到公眾平臺(tái)官網(wǎng)中的開發(fā)者中心頁配置授權(quán)回調(diào)域名。請(qǐng)注意,這里填寫的是域名(是一個(gè)字符串)甜橱,而不是URL享言,因此請(qǐng)勿加 http:// 等協(xié)議頭;
2渗鬼、授權(quán)回調(diào)域名配置規(guī)范為全域名览露,比如需要網(wǎng)頁授權(quán)的域名為:www.qq.com,配置以后此域名下面的頁面http://www.qq.com/music.html 譬胎、 http://www.qq.com/login.html 都可以進(jìn)行OAuth2.0鑒權(quán)差牛。但http://pay.qq.comhttp://music.qq.com 堰乔、 http://qq.com無法進(jìn)行OAuth2.0鑒權(quán)

關(guān)于網(wǎng)頁授權(quán)的兩種scope的區(qū)別說明:

1偏化、以snsapi_base為scope發(fā)起的網(wǎng)頁授權(quán),是用來獲取進(jìn)入頁面的用戶的openid的镐侯,并且是靜默授權(quán)并自動(dòng)跳轉(zhuǎn)到回調(diào)頁的侦讨。用戶感知的就是直接進(jìn)入了回調(diào)頁(往往是業(yè)務(wù)頁面)
2、以snsapi_userinfo為scope發(fā)起的網(wǎng)頁授權(quán)苟翻,是用來獲取用戶的基本信息的韵卤。但這種授權(quán)需要用戶手動(dòng)同意,并且由于用戶同意過崇猫,所以無須關(guān)注沈条,就可在授權(quán)后獲取該用戶的基本信息。
3诅炉、用戶管理類接口中的“獲取用戶基本信息接口”蜡歹,是在用戶和公眾號(hào)產(chǎn)生消息交互或關(guān)注后事件推送后,才能根據(jù)用戶OpenID來獲取用戶基本信息涕烧。這個(gè)接口月而,包括其他微信接口,都是需要該用戶(即openid)關(guān)注了公眾號(hào)后议纯,才能調(diào)用成功的父款。

關(guān)于網(wǎng)頁授權(quán)access_token和普通access_token的區(qū)別

1、微信網(wǎng)頁授權(quán)是通過OAuth2.0機(jī)制實(shí)現(xiàn)的痹扇,在用戶授權(quán)給公眾號(hào)后铛漓,公眾號(hào)可以獲取到一個(gè)網(wǎng)頁授權(quán)特有的接口調(diào)用憑證(網(wǎng)頁授權(quán)access_token),通過網(wǎng)頁授權(quán)access_token可以進(jìn)行授權(quán)后接口調(diào)用鲫构,如獲取用戶基本信息浓恶;
2、其他微信接口结笨,需要通過基礎(chǔ)支持中的“獲取access_token”接口來獲取到的普通access_token調(diào)用包晰。

具體而言湿镀,網(wǎng)頁授權(quán)流程分為四步:

1、引導(dǎo)用戶進(jìn)入授權(quán)頁面同意授權(quán)伐憾,獲取code
2勉痴、通過code換取網(wǎng)頁授權(quán)access_token(與基礎(chǔ)支持中的access_token不同)
3、如果需要树肃,開發(fā)者可以刷新網(wǎng)頁授權(quán)access_token蒸矛,避免過期
4、通過網(wǎng)頁授權(quán)access_token和openid獲取用戶基本信息(支持UnionID機(jī)制)

我們希望在用戶點(diǎn)擊自定義菜單時(shí),需要先獲取用戶的openid,以便從我們自己的后臺(tái)中通過該openid獲取這個(gè)用戶更多的信息,比如它對(duì)應(yīng)的我們后臺(tái)中的uid等胸嘴。
因此我們需要給這個(gè)自定義菜單按鈕添加一個(gè)對(duì)應(yīng)的URL,點(diǎn)擊這個(gè)菜單,跳轉(zhuǎn)到這個(gè)URL,這個(gè)URL會(huì)觸發(fā)獲取code操作,獲取到code后,通過獲取授權(quán)的access_token接口,獲取openid及access_token

(1) 給菜單添加url,及state映射關(guān)系
state為自定義字符串,可以用來標(biāo)識(shí)是用戶點(diǎn)擊了哪一個(gè)菜單,放在一個(gè)dict字典中,當(dāng)前我們制作的第一個(gè)菜單就對(duì)應(yīng) /page/index 映射,在wxconfig.py中添加

class WxConfig(object):
    """
    微信開發(fā)--基礎(chǔ)配置

    """
    AppID = 'wxxxxxxxxxxxxxxxx'  # AppID(應(yīng)用ID)
    AppSecret = 'xxxxxxxxxxxxxxxxxxxx'  # AppSecret(應(yīng)用密鑰)

    """微信網(wǎng)頁開發(fā)域名"""
    AppHost = 'http://xxxxxx.com'

    '''微信公眾號(hào)菜單映射數(shù)據(jù)'''
    """重定向后會(huì)帶上state參數(shù)雏掠,開發(fā)者可以填寫a-zA-Z0-9的參數(shù)值,最多128字節(jié)"""
    wx_menu_state_map = {
        'menuIndex0': '%s/page/index' % AppHost,  # 測(cè)試菜單1
}

在wxauthorize.py中添加授權(quán)認(rèn)證的類:

class WxAuthorServer(object):
    """
    微信網(wǎng)頁授權(quán)server

    get_code_url                            獲取code的url
    get_auth_access_token                   通過code換取網(wǎng)頁授權(quán)access_token
    refresh_token                           刷新access_token
    get_userinfo                            拉取用戶信息
    """

    """授權(quán)后重定向的回調(diào)鏈接地址劣像,請(qǐng)使用urlencode對(duì)鏈接進(jìn)行處理"""
    REDIRECT_URI = '%s/wx/wxauthor' % WxConfig.AppHost

    """
    應(yīng)用授權(quán)作用域
    snsapi_base (不彈出授權(quán)頁面乡话,直接跳轉(zhuǎn),只能獲取用戶openid)
    snsapi_userinfo (彈出授權(quán)頁面耳奕,可通過openid拿到昵稱绑青、性別、所在地屋群。并且闸婴,即使在未關(guān)注的情況下,只要用戶授權(quán)谓晌,也能獲取其信息)
    """
    SCOPE = 'snsapi_base'
    # SCOPE = 'snsapi_userinfo'

    """通過code換取網(wǎng)頁授權(quán)access_token"""
    get_access_token_url = 'https://api.weixin.qq.com/sns/oauth2/access_token?'

    """拉取用戶信息"""
    get_userinfo_url = 'https://api.weixin.qq.com/sns/userinfo?'


    def get_code_url(self, state):
        """獲取code的url"""
        dict = {'redirect_uri': self.REDIRECT_URI}
        redirect_uri = urllib.parse.urlencode(dict)
        author_get_code_url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&%s&response_type=code&scope=%s&state=%s#wechat_redirect' % (WxConfig.AppID, redirect_uri, self.SCOPE, state)
        logger.debug('【微信網(wǎng)頁授權(quán)】獲取網(wǎng)頁授權(quán)的code的url>>>>' + author_get_code_url)
        return author_get_code_url

    def get_auth_access_token(self, code):
        """通過code換取網(wǎng)頁授權(quán)access_token"""
        url = self.get_access_token_url + 'appid=%s&secret=%s&code=%s&grant_type=authorization_code' % (WxConfig.AppID, WxConfig.AppSecret, code)
        r = requests.get(url)
        logger.debug('【微信網(wǎng)頁授權(quán)】通過code換取網(wǎng)頁授權(quán)access_token的Response[' + str(r.status_code) + ']')
        if r.status_code == 200:
            res = r.text
            logger.debug('【微信網(wǎng)頁授權(quán)】通過code換取網(wǎng)頁授權(quán)access_token>>>>' + res)
            json_res = json.loads(res)
            if 'access_token' in json_res.keys():
                return json_res
            elif 'errcode' in json_res.keys():
                errcode = json_res['errcode']

    def get_userinfo(self, access_token, openid):
        """拉取用戶信息"""
        url = self.get_userinfo_url + 'access_token=%s&openid=%s&lang=zh_CN' % (access_token, openid)
        r = requests.get(url)
        logger.debug('【微信網(wǎng)頁授權(quán)】拉取用戶信息Response[' + str(r.status_code) + ']')
        if r.status_code == 200:
            res = r.text
            json_data = json.loads((res.encode('iso-8859-1')).decode('utf-8'))
            logger.debug('【微信網(wǎng)頁授權(quán)】拉取用戶信息>>>>' + str(json_data))

創(chuàng)建菜單的類文件:wxmenu.py

import requests
import json
from core.server.wxconfig import WxConfig
from core.cache.tokencache import TokenCache
from core.logger_helper import logger
from core.server.wxauthorize import WxAuthorServer


class WxMenuServer(object):
    """
    微信自定義菜單

    create_menu                     自定義菜單創(chuàng)建接口
    get_menu                        自定義菜單查詢接口
    delete_menu                     自定義菜單刪除接口
    create_menu_data                創(chuàng)建菜單數(shù)據(jù)
    """

    _token_cache = TokenCache()  # 微信token緩存
    _wx_author_server = WxAuthorServer()  # 微信網(wǎng)頁授權(quán)server

    def create_menu(self):
        """自定義菜單創(chuàng)建接口"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_create_url + access_token
            data = self.create_menu_data()
            r = requests.post(url, data.encode('utf-8'))
            logger.debug('【微信自定義菜單】自定義菜單創(chuàng)建接口Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義菜單】自定義菜單創(chuàng)建接口' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義菜單】自定義菜單創(chuàng)建接口獲取不到access_token')

    def get_menu(self):
        """自定義菜單查詢接口"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_get_url + access_token
            r = requests.get(url)
            logger.debug('【微信自定義菜單】自定義菜單查詢接口Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義菜單】自定義菜單查詢接口' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義菜單】自定義菜單查詢接口獲取不到access_token')

    def delete_menu(self):
        """自定義菜單刪除接口"""
        access_token = self._token_cache.get_cache(self._token_cache.KEY_ACCESS_TOKEN)
        if access_token:
            url = WxConfig.menu_delete_url + access_token
            r = requests.get(url)
            logger.debug('【微信自定義菜單】自定義菜單刪除接口Response[' + str(r.status_code) + ']')
            if r.status_code == 200:
                res = r.text
                logger.debug('【微信自定義菜單】自定義菜單刪除接口' + res)
                json_res = json.loads(res)
                if 'errcode' in json_res.keys():
                    errcode = json_res['errcode']
                    return errcode
        else:
            logger.error('【微信自定義菜單】自定義菜單刪除接口獲取不到access_token')

    def create_menu_data(self):
        """創(chuàng)建菜單數(shù)據(jù)"""
        menu_data = {'button': []}  # 大菜單
        menu_Index0 = {
            'type': 'view',
            'name': '測(cè)試菜單1',
            'url': self._wx_author_server.get_code_url('menuIndex0')
        }
        menu_data['button'].append(menu_Index0)
        MENU_DATA = json.dumps(menu_data, ensure_ascii=False)
        logger.debug('【微信自定義菜單】創(chuàng)建菜單數(shù)據(jù)MENU_DATA[' + str(MENU_DATA) + ']')
        return MENU_DATA

if __name__ == '__main__':
    wx_menu_server = WxMenuServer()
    '''創(chuàng)建菜單數(shù)據(jù)'''
    # wx_menu_server.create_menu_data()
    # '''自定義菜單創(chuàng)建接口'''
    wx_menu_server.create_menu()
    '''自定義菜單查詢接口'''
    # wx_menu_server.get_menu()
    '''自定義菜單刪除接口'''
    # wx_menu_server.delete_menu()

(2) 點(diǎn)擊菜單時(shí),觸發(fā)獲取code接口掠拳。微信公眾平臺(tái)攜帶code和state請(qǐng)求訪問我們后臺(tái)的 /wx/wxauthor 接口,根據(jù)state字段獲取 /page/index 映射,用來做重定向用癞揉。
通過code換取網(wǎng)頁授權(quán)access_token及openid,拿到openid后我們就可以重定向跳轉(zhuǎn)到 /page/index映射對(duì)應(yīng)的頁面 index.html

import tornado.web
from core.logger_helper import logger
from core.server.wxauthorize import WxConfig
from core.server.wxauthorize import WxAuthorServer
from core.cache.tokencache import TokenCache


class WxHandler(tornado.web.RequestHandler):
    """
    微信handler處理類
    """

    '''微信配置文件'''
    wx_config = WxConfig()
    '''微信網(wǎng)頁授權(quán)server'''
    wx_author_server = WxAuthorServer()
    '''redis服務(wù)'''
    wx_token_cache = TokenCache()

    def post(self, flag):

        if flag == 'wxauthor':
            '''微信網(wǎng)頁授權(quán)'''
            code = self.get_argument('code')
            state = self.get_argument('state')
            # 獲取重定向的url
            redirect_url = self.wx_config.wx_menu_state_map[state]
            logger.debug('【微信網(wǎng)頁授權(quán)】將要重定向的地址為:redirct_url[' + redirect_url + ']')
            logger.debug('【微信網(wǎng)頁授權(quán)】用戶同意授權(quán)纸肉,獲取code>>>>code[' + code + ']state[' + state + ']')
            if code:
                # 通過code換取網(wǎng)頁授權(quán)access_token
                data = self.wx_author_server.get_auth_access_token(code)
                openid = data['openid']
                logger.debug('【微信網(wǎng)頁授權(quán)】openid>>>>openid[' + openid + ']')
                if openid:
                    # 跳到自己的業(yè)務(wù)界面
                    self.redirect(redirect_url)
                else:
                    # 獲取不到openid
                    logger.debug('獲取不到openid')

七.菜單中網(wǎng)頁的開發(fā), JS-SDK的使用

在完成自定義菜單后,我們就可以開發(fā)自己的網(wǎng)頁了,在網(wǎng)頁中涉及到獲取用戶地理位置,微信支付等,都需要使用微信公眾平臺(tái)提供的JS-SDK,詳見 微信JS-SDK說明文檔

微信JS-SDK概述
微信JS-SDK是微信公眾平臺(tái)面向 網(wǎng)頁開發(fā)者 提供的基于微信內(nèi)的網(wǎng)頁開發(fā)工具包。
通過使用微信JS-SDK喊熟,網(wǎng)頁開發(fā)者可借助微信高效地使用拍照柏肪、選圖、語音芥牌、位置等手機(jī)系統(tǒng)的能力烦味,同時(shí)可以直接使用微信分享、掃一掃壁拉、卡券谬俄、支付 等微信特有的能力,為微信用戶提供更優(yōu)質(zhì)的網(wǎng)頁體驗(yàn)弃理。

JSSDK使用步驟
步驟一:綁定域名
先登錄微信公眾平臺(tái)進(jìn)入“公眾號(hào)設(shè)置”的“功能設(shè)置”里填寫“JS接口安全域名”溃论。
備注:登錄后可在“開發(fā)者中心”查看對(duì)應(yīng)的接口權(quán)限。
步驟二:引入JS文件
在需要調(diào)用JS接口的頁面引入如下JS文件痘昌,(支持https):http://res.wx.qq.com/open/js/jweixin-1.0.0.js 如需使用搖一搖周邊功能钥勋,請(qǐng)引入 http://res.wx.qq.com/open/js/jweixin-1.1.0.js
備注:支持使用 AMD/CMD 標(biāo)準(zhǔn)模塊加載方法加載
步驟三:通過config接口注入權(quán)限驗(yàn)證配置

注意:所有需要使用JS-SDK的頁面必須先注入配置信息炬转,否則將無法調(diào)用(同一個(gè)url僅需調(diào)用一次,對(duì)于變化url的SPA的web app可在每次url變化時(shí)進(jìn)行調(diào)用,目前Android微信客戶端不支持pushState的H5新特性算灸,所以使用pushState來實(shí)現(xiàn)web app的頁面會(huì)導(dǎo)致簽名失敗扼劈,此問題會(huì)在Android6.2中修復(fù))。

wx.config({
    debug: true, // 開啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶端alert出來菲驴,若要查看傳入的參數(shù)荐吵,可以在pc端打開,參數(shù)信息會(huì)通過log打出赊瞬,僅在pc端時(shí)才會(huì)打印捍靠。
    appId: '', // 必填,公眾號(hào)的唯一標(biāo)識(shí)
    timestamp: , // 必填森逮,生成簽名的時(shí)間戳
    nonceStr: '', // 必填榨婆,生成簽名的隨機(jī)串
    signature: '',// 必填,簽名褒侧,見附錄1
    jsApiList: [] // 必填良风,需要使用的JS接口列表,所有JS接口列表見附錄2
});

步驟四:通過ready接口處理成功驗(yàn)證

wx.ready(function(){
// config信息驗(yàn)證后會(huì)執(zhí)行ready方法闷供,所有接口調(diào)用都必須在config接口獲得結(jié)果之后烟央。  
    config是一個(gè)客戶端的異步操作,所以如果需要在頁面加載時(shí)就調(diào)用相關(guān)接口歪脏,則須把相關(guān)接口放在ready函數(shù)中調(diào)用來確保正確執(zhí)行疑俭。  
    對(duì)于用戶觸發(fā)時(shí)才調(diào)用的接口,則可以直接調(diào)用婿失,不需要放在ready函數(shù)中钞艇。
});  

步驟五:通過error接口處理失敗驗(yàn)證

wx.error(function(res){
    // config信息驗(yàn)證失敗會(huì)執(zhí)行error函數(shù),如簽名過期導(dǎo)致驗(yàn)證失敗豪硅,  
    具體錯(cuò)誤信息可以打開config的debug模式查看哩照,也可以在返回的res參數(shù)中查看,對(duì)于SPA可以在這里更新簽名懒浮。
});

網(wǎng)頁分享朋友圈實(shí)例:

wx.onMenuShareTimeline({
    title: '', // 分享標(biāo)題
    link: '', // 分享鏈接
    imgUrl: '', // 分享圖標(biāo)
    success: function () { 
    // 用戶確認(rèn)分享后執(zhí)行的回調(diào)函數(shù)
    },
    cancel: function () { 
    // 用戶取消分享后執(zhí)行的回調(diào)函數(shù)
    }
});

簽名算法:

簽名生成規(guī)則如下:參與簽名的字段包括noncestr(隨機(jī)字符串), 有效的jsapi_ticket, timestamp(時(shí)間戳), url(當(dāng)前網(wǎng)頁的URL飘弧,不包含#及其后面部分)。
對(duì)所有待簽名參數(shù)按照字段名的ASCII 碼從小到大排序(字典序)后砚著,使用URL鍵值對(duì)的格式(即key1=value1&key2=value2…)拼接成字符串string1次伶。
這里需要注意的是所有參數(shù)名均為小寫字符。對(duì)string1作sha1加密稽穆,字段名和字段值都采用原始值冠王,不進(jìn)行URL 轉(zhuǎn)義。
實(shí)現(xiàn):獲取JS-SDK權(quán)限簽名 wxsign.py

import time
import random
import string
import hashlib
from core.server.wxconfig import WxConfig
from core.cache.tokencache import TokenCache
from core.logger_helper import logger


class WxSign:
    """\
    微信開發(fā)--獲取JS-SDK權(quán)限簽名

    __create_nonce_str              隨機(jī)字符串
    __create_timestamp              時(shí)間戳
    sign                            生成JS-SDK使用權(quán)限簽名
    """

    def __init__(self, jsapi_ticket, url):
        self.ret = {
            'nonceStr': self.__create_nonce_str(),
            'jsapi_ticket': jsapi_ticket,
            'timestamp': self.__create_timestamp(),
            'url': url
        }

    def __create_nonce_str(self):
        return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15))

    def __create_timestamp(self):
        return int(time.time())

    def sign(self):
        string = '&'.join(['%s=%s' % (key.lower(), self.ret[key]) for key in sorted(self.ret)])
        self.ret['signature'] = hashlib.sha1(string.encode('utf-8')).hexdigest()
        logger.debug('【微信JS-SDK】獲取JS-SDK權(quán)限簽名>>>>dict[' + str(self.ret) + ']')
        return self.ret

if __name__ == '__main__':
    token_cache = TokenCache()  
    jsapi_ticket = token_cache.get_cache(token_cache.KEY_JSAPI_TICKET)
    # 注意 URL 一定要?jiǎng)討B(tài)獲取秧骑,不能 hardcode
    url = '%s/page/index' % WxConfig.AppHost
    sign = WxSign(jsapi_ticket, url)
    print(sign.sign())

八.完成測(cè)試版确,項(xiàng)目發(fā)布

將項(xiàng)目代碼傳到服務(wù)器扣囊,通過Superviso服務(wù)器啟動(dòng)run.py,然后用手機(jī)微信掃描關(guān)注中國銀通的服務(wù)號(hào)绒疗,進(jìn)行測(cè)試侵歇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吓蘑,隨后出現(xiàn)的幾起案子惕虑,更是在濱河造成了極大的恐慌,老刑警劉巖磨镶,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溃蔫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡琳猫,警方通過查閱死者的電腦和手機(jī)伟叛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脐嫂,“玉大人统刮,你說我怎么就攤上這事≌饲В” “怎么了威恼?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵坤检,是天一觀的道長(zhǎng)藏否。 經(jīng)常有香客問我门驾,道長(zhǎng),這世上最難降的妖魔是什么娃善? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任论衍,我火速辦了婚禮,結(jié)果婚禮上会放,老公的妹妹穿的比我還像新娘饲齐。我一直安慰自己,他們只是感情好咧最,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著御雕,像睡著了一般矢沿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酸纲,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天捣鲸,我揣著相機(jī)與錄音,去河邊找鬼闽坡。 笑死栽惶,一個(gè)胖子當(dāng)著我的面吹牛愁溜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播外厂,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼冕象,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了汁蝶?” 一聲冷哼從身側(cè)響起渐扮,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掖棉,沒想到半個(gè)月后墓律,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幔亥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年耻讽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帕棉。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡齐饮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笤昨,到底是詐尸還是另有隱情祖驱,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布瞒窒,位于F島的核電站捺僻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崇裁。R本人自食惡果不足惜匕坯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拔稳。 院中可真熱鬧葛峻,春花似錦、人聲如沸巴比。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻绞。三九已至采记,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間政勃,已是汗流浹背唧龄。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奸远,地道東北人既棺。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓讽挟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親丸冕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耽梅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容