本教程針對的是已掌握Python語言基本用法而且掌握其任一Web框架的用戶。html
本教程使用的Python版本爲3.5.0, Web框架爲Tornado, IDE開發工具爲PyCharm,整個開發過程是在Windows環境下測試開發,最終上線部署至centos服務器。前端
備註:(1) 若是您是python小白,建議參考 Python入門教程node
(2) 對tornado框架還不熟悉的同窗,建議參考 Tornado中文文檔python
1. Python開發環境和項目的初始化搭建;nginx
2. 微信公衆號註冊及開發模式校驗配置;git
3. 接收關注/取關事件推送和自動回覆;github
4. IOLoop定時獲取access_token和jsapi_ticket;web
5. 自定義菜單及點擊菜單時獲取openid;redis
6. 菜單中網頁的開發, JS-SDK的使用;算法
7. 完成測試,發佈上線,部署至centos服務器。
思惟導圖以下:
總體項目結構以下:
下面咱們正式進入詳細的開發流程
1. 安裝python及pip,並配置環境變量,安裝tornado框架
Python及pip安裝參考教程windows下面安裝Python和pip終極教程
(1) 下載Python包並安裝 點此下載
(2) 將python配置到系統環境變量
(3) 下載pip包並安裝 點此下載
(4) 將pip配置到系統環境變量
(5) 使用pip安裝tornado框架 指令爲:
pip install tornado
複製代碼
2. 選擇一款開發Python的IDE
本教程使用的是PyCharm點擊下載
附帶: PyCharm 2016.2.3專業版註冊碼
3. 選擇一個代碼託管平臺
本教程使用的是開源中國Git@osc代碼託管平臺 碼雲 - 開源中國代碼託管平臺,請自行註冊,並配置帳戶下的SSH密鑰,關於Git的使用,請參考教程 Git教程 - 廖雪峯的官方網站
4. 建立Web項目
使用Tornado搭建項目入口,端口號爲8000,項目搭建至完成微信校驗所需的基本代碼以下:
項目總體目錄
備註: 爲防止日誌輸出報錯, 請各位同窗注意修改日誌輸出目錄爲本身定義的文件目錄
import logging
from logging import Logger
from logging.handlers import TimedRotatingFileHandler
'''日誌管理類'''
def init_logger(logger_name):
if logger_name not in Logger.manager.loggerDict:
logger1 = logging.getLogger(logger_name)
logger1.setLevel(logging.INFO) # 設置最低級別
# logger1.setLevel(logging.DEBUG) # 設置最低級別
df = '%Y-%m-%d %H:%M:%S'
format_str = '[%(asctime)s]: %(name)s %(levelname)s %(lineno)s %(message)s'
formatter = logging.Formatter(format_str, df)
# handler all
try:
handler1 = TimedRotatingFileHandler('/usr/web_wx/log/all.log', when='D', interval=1, backupCount=7)
except Exception:
handler1 = TimedRotatingFileHandler('F:\program\web_wx\core\log\/all.log', when='D', interval=1, backupCount=7)
handler1.setFormatter(formatter)
handler1.setLevel(logging.DEBUG)
logger1.addHandler(handler1)
# handler error
try:
handler2 = TimedRotatingFileHandler('/usr/web_wx/log/error.log', when='D', interval=1, backupCount=7)
except Exception:
handler2 = TimedRotatingFileHandler('F:\program\web_wx\core\log\error.log', when='D', interval=1, backupCount=7)
handler2.setFormatter(formatter)
handler2.setLevel(logging.ERROR)
logger1.addHandler(handler2)
# console
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
# 設置日誌打印格式
console.setFormatter(formatter)
# 將定義好的console日誌handler添加到root logger
logger1.addHandler(console)
logger1 = logging.getLogger(logger_name)
return logger1
logger = init_logger('runtime-log')
if __name__ == '__main__':
logger.debug('test-debug')
logger.info('test-info')
logger.warn('test-warn')
logger.error('test-error')
複製代碼
from core.logger_helper import logger
import hashlib
import tornado.web
class WxSignatureHandler(tornado.web.RequestHandler):
""" 微信服務器簽名驗證, 消息回覆 check_signature: 校驗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校驗,signature='+signature+',×tamp='+timestamp+'&nonce='+nonce+'&echostr='+echostr)
result = self.check_signature(signature, timestamp, nonce)
if result:
logger.debug('微信sign校驗,返回echostr='+echostr)
self.write(echostr)
else:
logger.error('微信sign校驗,---校驗失敗')
except Exception as e:
logger.error('微信sign校驗,---Exception' + str(e))
def check_signature(self, signature, timestamp, nonce):
"""校驗token是否正確"""
token = 'test12345'
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
複製代碼
from core.server.wxauthorize import WxSignatureHandler
import tornado.web
'''web解析規則'''
urlpatterns = [
(r'/wxsignature', WxSignatureHandler), # 微信簽名
]
複製代碼
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()
複製代碼
(5) 同步項目文件至Git託管平臺
項目入口文件及微信校驗文件已編寫好,使用Git同步代碼至託管平臺,接下來須要配置端口映射,使外網能訪問到咱們的本地項目,便於完成微信服務端校驗.
5. 使用花生殼,配置本地測試所需端口映射
微信公衆號開發須要配置服務端URL, 驗證URL的有效性,這個URL必須以http://或https://開頭,分別支持80端口和443端口,咱們目前測試階段都是在本身電腦上測試(本地測試),爲了知足不斷修改代碼可以即時生效, 所以須要一個外網ip端口映射到本地(內網穿透),我本人使用的是花生殼內網穿透服務,下面是花生殼的使用流程:
(1) 花生殼的帳戶註冊 花生殼軟件-內網也能用!內網動態域名,註冊成功後,會贈送一個免費域名,這個域名同時也配備了一個公網ip
(2) 進入到花生殼管理界面, 選擇內網穿透菜單,進入到配置界面
(3) 選擇 右邊的"編輯"操做,彈出編輯映射面板,在"內網主機"一項,填上本身本地電腦的ip地址,端口填寫本身將要建立的web應用端口,我本地項目用的端口號爲8000,此處填寫8000便可
1. 微信公衆號註冊
官網連接mp.weixin.qq.com/,依次填寫信息進行註冊
2.微信公衆開發模式校驗配置
(1)登陸微信公衆號後, 進入基本配置,以下:
URL 填寫爲: 花生殼的域名+咱們項目中微信校驗的接口名:
XXXXXXX.imwork.net/wxsignature
token 填寫爲咱們項目中自定義的token: test12345
EncodingAESKey 點擊"隨機生成"按鈕便可,消息加密方式使用明文模式
填寫完畢後,先啓動咱們的項目,運行python run.py指令後, 保證咱們的服務器是運行着的, 而後點擊"提交",若是你是按照以上流程操做的話,會提示提交成功,不然校驗失敗,須要咱們經過日誌檢查是哪一塊出了問題.
(2) 接下來,校驗成功後,點擊啓用,便可激活開發者模式
1. 接收關注/取關事件推送
在開發模式中,有新用戶關注咱們的公衆號時,微信公衆平臺會使用http協議的Post方式推送數據至咱們的後臺微信校驗的接口,在接收到消息後,咱們後臺發送一條歡迎語給該用戶,關於微信公衆平臺推送消息的具體內容和數據格式,詳見微信開發文檔
如下是在該文件中增長的post方法,用來接收事件推送
def post(self):
body = self.request.body
logger.debug('微信消息回覆中心】收到用戶消息' + 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':
# 關注事件
CreateTime = int(time.time())
reply_content = '歡迎關注個人公衆號~'
out = self.reply_text(FromUserName, ToUserName, CreateTime, reply_content)
self.write(out)
except:
pass
def reply_text(self, FromUserName, ToUserName, CreateTime, Content):
"""回覆文本消息模板"""
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. 自動回覆
(1) 同接收關注/取關事件推送消息同樣,用戶給咱們公衆號發送消息時,微信公衆平臺也會推送數據至咱們的後臺微信校驗的接口,在接收到消息後,咱們取出自定義的關鍵字進行匹配,匹配到了就執行自動回覆
(2) 微信公衆平臺也提供了語音識別功能, 將用戶發送的語音內容識別轉化爲文字,發送給咱們後臺,在使用該功能時須要在接口權限中打開語音識別功能.
如下是在該文件中post方法中增長的一個判斷,用來匹配用戶文本消息和語音消息中的關鍵字
def post(self):
body = self.request.body
logger.debug('微信消息回覆中心】收到用戶消息' + 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 # 文本消息內容
elif MsgType == 'voice':
Content = data.find('Recognition').text # 語音識別結果,UTF8編碼
if Content == u'你好':
reply_content = '您好,請問有什麼能夠幫助您的嗎?'
else:
# 查找不到關鍵字,默認回覆
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':
# 關注事件
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):
"""回覆文本消息模板"""
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
複製代碼
1. access_token
access_token是公衆號的全局惟一票據,公衆號調用各接口時都需使用access_token。開發者須要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前爲2個小時,需定時刷新,重複獲取將致使上次獲取的access_token失效。如下是文檔中的說明 詳見微信開發文檔
2. jsapi_ticket
jsapi_ticket是公衆號用於調用微信JS接口的臨時票據。正常狀況下,jsapi_ticket的有效期爲7200秒,經過access_token來獲取。因爲獲取jsapi_ticket的api調用次數很是有限,頻繁刷新jsapi_ticket會致使api調用受限,影響自身業務,開發者必須在本身的服務全局緩存jsapi_ticket 。參考文檔JS-SDK使用權限簽名算法
3. Redis數據庫
若是有對Redis不瞭解的同窗,可參考Redis快速入門
import redis
"""緩存服務器"""
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
複製代碼
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過時時間, 2小時
_expire_js_token = 30 * 24 * 3600 # 微信js網頁受權過時時間, 30天
KEY_ACCESS_TOKEN = 'access_token' # 微信全局惟一票據access_token
KEY_JSAPI_TICKET = 'jsapi_ticket' # JS_SDK權限簽名的jsapi_ticket
def set_access_cache(self, key, value):
"""添加微信access_token驗證相關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):
"""添加網頁受權相關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 實現定時獲取access_token和 jsapi_ticket,並將獲取到的access_token和 jsapi_ticket保存在Redis數據庫中
class WxConfig(object):
""" 微信開發--基礎配置 """
AppID = 'wxxxxxxxxxxxxxxxx' # AppID(應用ID)
AppSecret = '024a7fcxxxxxxxxxxxxxxxxxxxx' # AppSecret(應用密鑰)
'''獲取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)
複製代碼
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):
""" 定時任務調度器 excute 執行定時器任務 get_access_token 獲取微信全局惟一票據access_token get_jsapi_ticket 獲取JS_SDK權限簽名的jsapi_ticket """
_token_cache = TokenCache() # 微信token緩存實例
_expire_time_access_token = 7000 * 1000 # token過時時間
def excute(self):
"""執行定時器任務"""
logger.info('【獲取微信全局惟一票據access_token】>>>執行定時器任務')
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_access_token(self):
"""獲取微信全局惟一票據access_token"""
url = WxConfig.config_get_access_token_url
r = requests.get(url)
logger.info('【獲取微信全局惟一票據access_token】Response[' + str(r.status_code) + ']')
if r.status_code == 200:
res = r.text
logger.info('【獲取微信全局惟一票據access_token】>>>' + res)
d = json.loads(res)
if 'access_token' in d.keys():
access_token = d['access_token']
# 添加至redis中
self._token_cache.set_access_cache(self._token_cache.KEY_ACCESS_TOKEN, access_token)
# 獲取JS_SDK權限簽名的jsapi_ticket
self.get_jsapi_ticket()
return access_token
elif 'errcode' in d.keys():
errcode = d['errcode']
logger.info(
'【獲取微信全局惟一票據access_token-SDK】errcode[' + errcode + '] , will retry get_access_token() method after 10s')
tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)
else:
logger.error('【獲取微信全局惟一票據access_token】request access_token error, will retry get_access_token() method after 10s')
tornado.ioloop.IOLoop.instance().call_later(10, self.get_access_token)
def get_jsapi_ticket(self):
"""獲取JS_SDK權限簽名的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權限簽名的jsapi_ticket的Response[' + str(r.status_code) + ']')
if r.status_code == 200:
res = r.text
logger.info('【微信JS-SDK】獲取JS_SDK權限簽名的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權限簽名的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權限簽名的jsapi_ticket時,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()
"""執行定時器"""
wx_shedule.excute()
複製代碼
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)
# 執行定時任務
wx_shedule = WxShedule()
wx_shedule.excute()
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
複製代碼
1. 編寫菜單對應的html頁面
2.建立一個菜單,並給菜單添加獲取受權code的URL
如下是微信公衆平臺官方文檔給出的具體流程,詳見 網頁受權獲取用戶基本信息
咱們但願在用戶點擊自定義菜單時,須要先獲取用戶的openid,以便從咱們本身的後臺中經過該openid獲取這個用戶更多的信息,好比它對應的咱們後臺中的uid等, 若是咱們後臺中沒有這個用戶,則須要執行綁定等操做.
所以咱們須要給這個自定義菜單按鈕添加一個對應的URL,點擊這個菜單,跳轉到這個URL,這個URL會觸發獲取code操做,獲取到code後,經過獲取受權的access_token接口,獲取openid及access_token
(1) 給菜單添加url,及state映射關係
(2) 點擊菜單時,觸發獲取code接口,微信公衆平臺攜帶code和state請求訪問咱們後臺的 /wx/wxauthor 接口,根據state字段獲取 /page/index 映射,用來作重定向用.經過code換取網頁受權access_token及openid,拿到openid後咱們就能夠重定向跳轉到 /page/index映射對應的頁面 index.html
附:涉及到的主要程序代碼以下:
class WxConfig(object):
""" 微信開發--基礎配置 """
AppID = 'wxxxxxxxxxxxxxxxx' # AppID(應用ID)
AppSecret = '024a7fcxxxxxxxxxxxxxxxxxxxx' # AppSecret(應用密鑰)
"""微信網頁開發域名"""
AppHost = 'http://xxxxxx.com'
'''獲取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)
'''自定義菜單建立接口'''
menu_create_url = 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token='
'''自定義菜單查詢接口'''
menu_get_url = 'https://api.weixin.qq.com/cgi-bin/menu/get?access_token='
'''自定義菜單刪除接口'''
menu_delete_url = 'https://api.weixin.qq.com/cgi-bin/menu/delete?access_token='
'''微信公衆號菜單映射數據'''
"""重定向後會帶上state參數,開發者能夠填寫a-zA-Z0-9的參數值,最多128字節"""
wx_menu_state_map = {
'menuIndex0': '%s/page/index' % AppHost, # 測試菜單1
}
複製代碼
class WxAuthorServer(object):
""" 微信網頁受權server get_code_url 獲取code的url get_auth_access_token 經過code換取網頁受權access_token refresh_token 刷新access_token check_auth 檢驗受權憑證(access_token)是否有效 get_userinfo 拉取用戶信息 """
"""受權後重定向的回調連接地址,請使用urlencode對連接進行處理"""
REDIRECT_URI = '%s/wx/wxauthor' % WxConfig.AppHost
""" 應用受權做用域 snsapi_base (不彈出受權頁面,直接跳轉,只能獲取用戶openid) snsapi_userinfo (彈出受權頁面,可經過openid拿到暱稱、性別、所在地。而且,即便在未關注的狀況下,只要用戶受權,也能獲取其信息) """
SCOPE = 'snsapi_base'
# SCOPE = 'snsapi_userinfo'
"""經過code換取網頁受權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('【微信網頁受權】獲取網頁受權的code的url>>>>' + author_get_code_url)
return author_get_code_url
def get_auth_access_token(self, code):
"""經過code換取網頁受權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('【微信網頁受權】經過code換取網頁受權access_token的Response[' + str(r.status_code) + ']')
if r.status_code == 200:
res = r.text
logger.debug('【微信網頁受權】經過code換取網頁受權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']
複製代碼
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 自定義菜單建立接口 get_menu 自定義菜單查詢接口 delete_menu 自定義菜單刪除接口 create_menu_data 建立菜單數據 """
_token_cache = TokenCache() # 微信token緩存
_wx_author_server = WxAuthorServer() # 微信網頁受權server
def create_menu(self):
"""自定義菜單建立接口"""
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('【微信自定義菜單】自定義菜單建立接口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 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):
"""建立菜單數據"""
menu_data = {'button': []} # 大菜單
menu_Index0 = {
'type': 'view',
'name': '測試菜單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('【微信自定義菜單】建立菜單數據MENU_DATA[' + str(MENU_DATA) + ']')
return MENU_DATA
if __name__ == '__main__':
wx_menu_server = WxMenuServer()
'''建立菜單數據'''
# wx_menu_server.create_menu_data()
# '''自定義菜單建立接口'''
wx_menu_server.create_menu()
'''自定義菜單查詢接口'''
# wx_menu_server.get_menu()
'''自定義菜單刪除接口'''
# wx_menu_server.delete_menu()
複製代碼
wx_handler.py
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()
'''微信網頁受權server'''
wx_author_server = WxAuthorServer()
'''redis服務'''
wx_token_cache = TokenCache()
def post(self, flag):
if flag == 'wxauthor':
'''微信網頁受權'''
code = self.get_argument('code')
state = self.get_argument('state')
# 獲取重定向的url
redirect_url = self.wx_config.wx_menu_state_map[state]
logger.debug('【微信網頁受權】將要重定向的地址爲:redirct_url[' + redirect_url + ']')
logger.debug('【微信網頁受權】用戶贊成受權,獲取code>>>>code[' + code + ']state[' + state + ']')
if code:
# 經過code換取網頁受權access_token
data = self.wx_author_server.get_auth_access_token(code)
openid = data['openid']
logger.debug('【微信網頁受權】openid>>>>openid[' + openid + ']')
if openid:
# 跳到本身的業務界面
self.redirect(redirect_url)
else:
# 獲取不到openid
logger.debug('獲取不到openid')
複製代碼
在完成自定義菜單後,咱們就能夠開發本身的網頁了,在網頁中涉及到獲取用戶地理位置,微信支付等,都須要使用微信公衆平臺提供的JS-SDK,詳見 微信JS-SDK說明文檔
1. 獲取JS-SDK權限簽名
import time
import random
import string
import hashlib
from core.server.weixin.wxconfig import WxConfig
from core.server.cache.tokencache import TokenCache
from core.logger_helper import logger
class WxSign:
"""\ 微信開發--獲取JS-SDK權限簽名 __create_nonce_str 隨機字符串 __create_timestamp 時間戳 sign 生成JS-SDK使用權限簽名 """
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權限簽名>>>>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 必定要動態獲取,不能 hardcode
url = '%s/order/index' % WxConfig.AppHost
sign = WxSign(jsapi_ticket, url)
print(sign.sign())
複製代碼
本測試項目發佈上線時使用的服務器爲阿里雲的centos 6.5服務器,系統python版本爲2.7,爲保證多個Python版本共存,及當前項目環境的純淨,須要使用pyenv及虛擬環境virtualenv
同時咱們採用 nginx 作負載均衡和靜態文件伺服,supervisor作守護進程管理
1. 安裝pyenv
(1) 下載
$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
複製代碼
(2) 配置
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc
複製代碼
(3) 從新加載shell
$ exec $SHELL -l
複製代碼
2.經過pyenv安裝多個版本的python
(1) 安裝相關依賴
yum install readline readline-devel readline-static -y
yum install openssl openssl-devel openssl-static -y
yum install sqlite-devel -y
yum install bzip2-devel bzip2-libs -y
複製代碼
(2) 安裝須要的python版本
pyenv install 3.5.0
而後刷新python版本
pyenv rehash
複製代碼
(3) 設置全局Python版本
$ pyenv global 3.5.0
複製代碼
3.使用虛擬環境virtualenv
(1) 安裝
$ pip install virtualenv
複製代碼
(2) 使用方法
(a)進入項目目錄
$ virtualenv --no-site-packages wx_env
複製代碼
(b)用source進入該環境
$ source wx_env/bin/activate
複製代碼
注意到命令提示符變了,有個(venv)前綴,表示當前環境是一個名爲venv的Python環境
4.導出本地項目的關聯關係,並在centos上安裝
pip freeze > ./requirements.txt
pip install -r requirements.txt
複製代碼
5.安裝redis服務
(1)下載redis Redis
(2) 上傳至 /usr/local文件夾
(3) 解壓 tar -xzvf redis-3.2.3.tar.gz
(4) 重命名redis-3.2.3文件名爲redis
(4) 進入目錄 cd /usr/local/redis
(5) 編譯安裝
make
make install
複製代碼
(6) 修改配置文件
vi /etc/redis/redis.conf
複製代碼
僅修改: daemonize yes (no-->yes)
(7) 啓動
/usr/local/bin/redis-server /usr/local/redis/redis.conf
複製代碼
(8) 查看啓動
ps -ef | grep redis
複製代碼
6.安裝nginx
(1) 下載安裝包:
wget http://nginx.org/download/nginx-1.10.0.tar.gz
複製代碼
(2) 解壓Nginx的tar包,並進入解壓好的目錄
tar -zxvf nginx-1.10.0.tar.gz
cd nginx-1.10.0/
複製代碼
(3) 安裝zlib和pcre庫
yum -y install zlib zlib-devel
yum -y install pcre pcre-devel
複製代碼
(4) 配置、編譯並安裝
./configure
複製代碼
make
make install
複製代碼
(5) 啓動nginx
/usr/local/nginx/sbin/nginx
複製代碼
訪問服務器後以下圖顯示說明Nginx運正常。
7.配置nginx
user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
upstream web_wx {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
server 127.0.0.1:8004;
}
sendfile on; #tcp_nopush on; keepalive_timeout 65;
proxy_read_timeout 200;
tcp_nopush on;
tcp_nodelay on;
gzip on;
gzip_min_length 1000;
gzip_proxied any;
server {
listen 80;
server_name localhost; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location / {
proxy_pass_header Server;
proxy_set_header Host $http_host; # proxy_redirect false; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://web_wx; }
}
}
複製代碼
8.配置Supervisord
(1) 安裝
yum install supervisor
複製代碼
(2) 設置開機自啓
wget -O /etc/rc.d/init.d/supervisord https://gist.githubusercontent.com/gracece/21e5719b234929799eeb/raw/supervisord
複製代碼
(3)將守護進程添加爲服務
chmod +x /etc/rc.d/init.d/supervisord
chkconfig --add supervisord #加爲服務
ntsysv #運行ntsysv,選中supervisord啓動系統時跟着啓動。
複製代碼
(4) 設置 /etc/supervisord.conf文件
[unix_http_server]
file=/tmp/supervisor.sock ; (the path to the socket file)
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; (default is no username (open server))
;password=123 ; (default is no password (open server))
;[inet_http_server] ; inet (TCP) server disabled by default
;port=127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface)
;username=user ; (default is no username (open server))
;password=123 ; (default is no password (open server))
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
;umask=022 ; (process file creation umask;default 022)
;user=chrism ; (default is current user, required if root)
;identifier=supervisor ; (supervisord identifier, default is 'supervisor')
;directory=/tmp ; (default is not to cd during start)
;nocleanup=true ; (don't clean up tempfiles at start;default false) ;childlogdir=/tmp ; ('AUTO' child log dir, default $TEMP) ;environment=KEY="value" ; (key value pairs to add to environment) ;strip_ansi=false ; (strip ansi escape codes in logs; def. false) [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket ;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket ;username=grace ; should be same as http_username if set ;password=grace ; should be same as http_password if set ;prompt=mysupervisor ; cmd line prompt (default "supervisor") ;history_file=~/.sc_history ; use readline history if available [group:tornadoApp] programs=web_wx [program:web_wx ] command=python /var/web_wx/run.py --port=80%(process_num)02d directory=/var/web_wx/ process_name = %(program_name)s%(process_num)d autorestart=true redirect_stderr=true stdout_logfile=/var/log/tornado.log stdout_logfile_maxbytes=500MB stdout_logfile_backups=50 stderr_logfile=/var/log/tornado-error.log loglevel=info numprocs = 4 numprocs_start = 1 複製代碼
(5) 開啓守護進程服務
supervisord
supervisorctl reload all
supervisorctl status複製代碼
福利: 本文已同步到個人我的技術網站 IT乾貨-sufaith 該網站包括Python, Linux, Nodejs, 前端開發等模塊, 專一於程序開發中的技術、經驗總結與分享, 歡迎訪問.