微信服務(wù)號(hào)開發(fā)
整體流程
- 域名報(bào)備稚配,服務(wù)器搭建
- Python開發(fā)環(huán)境和項(xiàng)目的初始化搭建体箕;
- 微信公眾號(hào)注冊(cè)及開發(fā)模式校驗(yàn)配置;
- 接收關(guān)注/取關(guān)事件推送和自動(dòng)回復(fù)图柏;
- IOLoop定時(shí)獲取access_token和jsapi_ticket;
- 自定義菜單及點(diǎn)擊時(shí)獲取openid及用戶信息任连;
- 菜單中網(wǎng)頁的開發(fā), JS-SDK的使用蚤吹;
- 完成測(cè)試,發(fā)布上線,部署至服務(wù)器。
思維導(dǎo)圖:
一.域名報(bào)備随抠,服務(wù)器搭建
微信要求的域名是備案過的域名裁着,且要下載微信提供的檢測(cè)文件,放在搭建的服務(wù)器下拱她,然后對(duì)域名進(jìn)行測(cè)試二驰,通過才能添加。
搭建項(xiàng)目服務(wù)器 Nginx+Tornado+Supervisor 部署操作方法
搭建Redis服務(wù)器 進(jìn)行數(shù)據(jù)存儲(chǔ) Redis安裝方法
二.Python開發(fā)環(huán)境搭建(Linux系統(tǒng))及項(xiàng)目初始化搭建
-
安裝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
-
創(chuàng)建微信服務(wù)號(hào)的后臺(tái)項(xiàng)目椭懊,使用Tornado搭建項(xiàng)目入口,端口號(hào)為8000
微信服務(wù)端校驗(yàn)的接口文件 wxauthorize.pyimport 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+',×tamp='+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)配置
- 微信公眾號(hào)注冊(cè)
官網(wǎng)鏈接https://mp.weixin.qq.com,需要注冊(cè)為服務(wù)號(hào)诸蚕,依次填寫信息進(jìn)行注冊(cè) - 微信公眾開發(fā)模式校驗(yàn)配置
(1)登錄微信公眾號(hào)后, 進(jìn)入基本配置,如下:
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ù)步势;
- 接收關(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
- 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ā)文檔 - 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)限簽名算法 - 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
-
編寫菜單對(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)
]
-
創(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.com 、 http://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è)試侵歇。