摘要:最近在做一個微信預約洗車的項目,其中有個功能是預約完成后給用戶發(fā)一個模板消息,發(fā)送模板消息需要以及格式的消息內(nèi)容,接口如下。關(guān)于微信的介紹是公眾號的全局唯一票據(jù),公眾號調(diào)用各接口時都需使用。
最近在做一個微信預約洗車的項目,其中有個功能是預約完成后給用戶發(fā)一個模板消息,發(fā)送模板消息需要AccessToken以及json格式的消息內(nèi)容,接口如下。
發(fā)送模板消息
接口調(diào)用請求說明
http請求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKENPOST數(shù)據(jù)說明
POST數(shù)據(jù)示例如下:
{ "touser":"OPENID", "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", "url":"http://weixin.qq.com/download", "data":{ "first": { "value":"恭喜你購買成功!", "color":"#173177" }, "keynote1":{ "value":"巧克力", "color":"#173177" }, "keynote2": { "value":"39.8元", "color":"#173177" }, "keynote3": { "value":"2014年9月22日", "color":"#173177" }, "remark":{ "value":"歡迎再次購買!", "color":"#173177" } } }返回碼說明
在調(diào)用模板消息接口后,會返回JSON數(shù)據(jù)包。正常時的返回JSON數(shù)據(jù)包示例:
{ "errcode":0, "errmsg":"ok", "msgid":200228332 }
我而同事已經(jīng)寫過這個功能了,索性就直接拿來用了。但是在使用的過程中,發(fā)現(xiàn)第一次可以成功發(fā)送模板消息,第二次就返回 errcode 40001,token驗證失敗。
關(guān)于微信AccessToken的介紹:
access_token是公眾號的全局唯一票據(jù),公眾號調(diào)用各接口時都需使用access_token。開發(fā)者需要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前為2個小時,需定時刷新,重復獲取將導致上次獲取的access_token失效。(注:獲取access_token接口的每日調(diào)用限額為2000次)
初步懷疑是不是別的地方更新了AccessToken,于是我打開他的代碼,如下(偽代碼):
public String getAccessToken(){ String token = (String)request.getSession().get(Const.ACCESS_TOKEN); if(token 為空){ toekn = getTokenFormWx(); request.getSession().add(Const.ACCESS_TOKEN,token). return token; } return token; }
這樣寫看起來好像沒什么問題,也不是每次都去獲取一個新的access_token。但他忽略了一點,session并不是只有一份的,系統(tǒng)為每個會話都創(chuàng)建一個多帶帶的session,最后調(diào)用getAccessToken的會話讓其他會話的session中的access_token都失效了。
我決定動手把代碼修改了一下,因為access_token的有效時間是7200秒,當時想著也放在redis里面好了,可以利用redis的自動過期來保證access_token的有效性,但是項目中沒有使用redis,加進來也是大材小用了,最后想想還是放在了ServletContext里面。
ServletContext,是一個全局的儲存信息的空間,服務(wù)器開始,其就存在,服務(wù)器關(guān)閉,其才釋放。request,一個用戶可有多個;session,一個用戶一個;而servletContext,所有用戶共用一個。所以,為了節(jié)省空間,提高效率,ServletContext中,要放必須的、重要的、所有用戶需要共享的線程又是安全的一些信息。
于是就有了下面這段代碼(偽)
public String getAccessToken(){ MapcacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*7000){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 為空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
這樣看起來好像是比之前的代碼好了一點,不會為沒一個會話都創(chuàng)建一個access_token,而且保證了時效性。但其實還是存在一點問題的,假如有兩個線程同時調(diào)用了這一個方法,其中第一個線程進了if在調(diào)用getTokenFormWx()的時候因為網(wǎng)絡(luò)或者其他原因等在這里了,第二個線程來了還是進了if,并且成功的調(diào)用getTokenFormWx()返回了token給調(diào)用者處理業(yè)務(wù)邏輯,這時候第一個線程執(zhí)行完畢,刷新了token,這樣就導致了第二個線程的token已經(jīng)失效,在處理業(yè)務(wù)邏輯的時候必然失敗。
我們有沒有辦法避免這個問題呢?當然是有的。
你想我直接使用synchronized好了,加在方法上,這樣就不會錯了。于是方法就變成了這樣
public synchronized String getAccessToken(){ MapcacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 為空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
這樣是能解決問題,但是解決問題代價也太大了,每一個線程想要獲取這個token就得等其他線程全部獲取完才能拿到,大大降低了效率,不可行的。所以再次改動代碼,變成了下面這樣。
public String getAccessToken(){ MapcacheMap = request.getServletContext().getAttr(Const.WX_TOKEN_MAP); if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ synchronized(this){ if(cacheMap==null || System.currentTimeMillis()-(Date)cacheMap.get(Const.WX_TOKEN_TIME).getTime()>1000*4800){ cacheMap = new HashMap<>(); String token = getTokenFormWx(); if(token 為空){ throw new RuntimeException("AccessToken is null"); } cacheMap.put(Const.WX_TOKEN_VAL,token); cacheMap.put(Const.WX_TOKEN_TIME,new Date()); } } } return (String)cacheMap.get(Const.WX_TOKEN_VAL); }
當?shù)谝粋€線程進了if之后,執(zhí)行synchronized里面的代碼,等待在了getTokenFormWx(),第二個線程也進了if,但由于加了synchronized,所以會等待在那里,等第一個線程處理完它才能執(zhí)行,第一個線程執(zhí)行完畢之后返回token去執(zhí)行業(yè)務(wù)邏輯,第二個線程進入synchronized代碼塊,執(zhí)行這里面的if判斷,由于第一個線程已經(jīng)成功獲取token并且刷新了ServletContext中的cacheMap,條件已經(jīng)不滿足,所以第二個線程是無法執(zhí)行這個if里面的代碼了,到此我們就設(shè)計了一個線程安全的獲取access_token方案。
看樣子好像一切都ok了,但是在測試后還是會出現(xiàn)一樣的問題。
我又仔細檢查了兩遍代碼,還是沒有發(fā)現(xiàn)有問題的地方。找不到錯誤的地方,我決定開始試錯。
第一次,我把https://api.weixin.qq.com/cgi...改成https://api.weixin.qq.com/cgi...
參數(shù)access_token放入post請求參數(shù)里面,其他參數(shù)放進request body里面。
結(jié)果:第一次就返回了40001 access_token無效。
第二次,我把https://api.weixin.qq.com/cgi...改成https://api.weixin.qq.com/cgi...
參數(shù)access_token放入post請求參數(shù)里面并使用trim()去除空格,其他參數(shù)放進request body里面。
結(jié)果:第一次就返回了40001 access_token無效。
第三次,我把https://api.weixin.qq.com/cgi...
其他參數(shù)放進request body里面。
結(jié)果:一切ok。。。。
為什么會多了空格?我也很想知道,但由于調(diào)試了太久時間,已經(jīng)很晚了,而第二天就是假期,所以我也就沒有深究了。
那為什么第二次和第三次都對ACCESS_TOKEN進行了去空格處理,為什么返回的結(jié)果卻不一樣呢?
這就得不得不說一下Http協(xié)議了,但這里不需要講太多,所以我們只說一下Http協(xié)議之請求消息Request。
客戶端發(fā)送一個HTTP請求到服務(wù)器的請求消息包括以下格式:
請求行(request line)、請求頭部(header)、空行和請求數(shù)據(jù)四個部分組成。
圖片描述
Get請求例子(java按得票排序)
GET https://segmentfault.com/t/java?type=votes HTTP/1.1 Host: segmentfault.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.2372.400 QQBrowser/9.5.10548.400 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8 Referer: https://segmentfault.com/t/java Accept-Encoding: gzip, deflate, sdch, br Accept-Language: zh-CN,zh;q=0.8 Cookie: 這個我就不貼出來了
Post請求例子(添加筆記)
POST https://segmentfault.com/api/notes/add?_=6e0a1202503bc4d86e63672cff567b81 HTTP/1.1 Host: segmentfault.com Connection: keep-alive Content-Length: 139 Accept: application/json, text/javascript, /; q=0.01 Origin: https://segmentfault.com X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.2372.400 QQBrowser/9.5.10548.400 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Referer: https://segmentfault.com/record Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.8 Cookie: 這個真的不能貼 title=%E6%B5%8B%E8%AF%95%E7%AC%94%E8%AE%B0&text=%E6%B5%8B%E8%AF%95%E7%AC%94%E8%AE%B0&id=&draftId=1220000008931250&isPrivate=0&language=text
對比一下你發(fā)現(xiàn)了什么?
get請求參數(shù)在url后面,使用?當作標志,多個參數(shù)使用&分割 類似?a=1&b=2
post參數(shù)在請求頭部空一行的后面 類似 a=1&b=2
那post提交的json串在哪個位置呢?
其實你已經(jīng)知道啦,也是在請求頭部空一行的后面 不過是以json的格式,而服務(wù)器內(nèi)部使用&分割參數(shù),使得開發(fā)者可以使用getParameter獲取提交的參數(shù),而其他類型的參數(shù)(例如json串和xml)開發(fā)者可以使用getInputStream來讀取到參數(shù)然后自己解析。
那post請求能否把參數(shù)寫在url后面呢?就像 post?a=1&b=2
答案是可以的,服務(wù)器可以成功解析到。
那get請求能把參數(shù)寫在request body里面嗎?
答案是否定的,服務(wù)器對get請求只解析url后面的,request body里面的他不關(guān)心。
那你發(fā)送模板消息的參數(shù)為什么寫在request body里面就不行呢?
我也不知道微信內(nèi)部是怎么做的,但是我覺得吧,微信之所以要把access_token寫在url后面,因為這個接口request body里面是模板消息的json串 如果再把access_token加進去 數(shù)據(jù)大概會是這樣
access_toke=xxxxxxxxxxx {"touser":"OPENID","template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", "url":"http://weixin.qq.com/download", ... }
微信方面也不好分割這個串,于是他們覺得要這個access_token寫在url后面,他們獲取到url后再手動分割處理,request body里面就只放純json串,解析起來也很方便。這就是為什么我第二次操作失敗的原因啦。
第一次寫技術(shù)類得文章,文筆不好多多見諒。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/66936.html
摘要:訪問令牌為提升系統(tǒng)安全性,配置中心和客戶端進行安全性校驗,雙方匹配才允許通訊啟動時,優(yōu)先全量加載鏡像數(shù)據(jù)到層,避免逐個請求耗時簡介是一個輕量級分布式配置管理平臺,擁有輕量級秒級動態(tài)推送多環(huán)境多語言配置監(jiān)聽權(quán)限控制版本回滾等特性。 Release Notes 1、輕量級改造:廢棄ZK,改為 DB + 磁盤 + long polling 方案,部署更輕量,學習更簡單;集群部署更方便,與單...
摘要:第三方登錄是現(xiàn)在常見的登錄方式,免注冊且安全方便快捷。大部分的第三方登錄都參考了的認證方法。這里我主要總結(jié)一下第三方登錄組件的設(shè)計流程。身份認證組件,需解耦,至少要喚起登錄和登出事件。認證成功喚起登錄事件并將用戶信息傳遞出去。 第三方登錄是現(xiàn)在常見的登錄方式,免注冊且安全方便快捷。 本篇文章將以Github為例,介紹如何在自己的站點添加第三方登錄模塊。 OAuth2.0 OAuth(開...
摘要:基于的框架仿的社區(qū)服務(wù)端渲染,主要是為了優(yōu)化以及首屏加載速度線上地址地址技術(shù)棧目錄結(jié)構(gòu)配置文件封裝工具函數(shù)滾動條操作函數(shù)靜態(tài)資源實例化之前執(zhí)行的插件注冊全局組件注冊全局服務(wù)端渲染時保存供服務(wù)端請求時的獲取頁面級組件首頁登錄頁未讀消 nuxt-cnode 基于vue的nuxt框架仿的cnode社區(qū)服務(wù)端渲染,主要是為了seo優(yōu)化以及首屏加載速度 線上地址 http://nuxt-cnod...
摘要:源碼非常簡單談?wù)剬崿F(xiàn)的問題當請求上線文沒有如果調(diào)用會直接,這個肯定會報錯,因為上下文失敗如果設(shè)置線程隔離,這里也會報錯。導致安全上下問題傳遞不到子線程中。歡迎關(guān)注我們獲得更多的好玩實踐 背景分析 showImg(https://segmentfault.com/img/remote/1460000018899024?w=494&h=245); 1.客戶端攜帶認證中心發(fā)放的token,...
閱讀 2940·2021-11-04 16:06
閱讀 774·2021-09-30 09:56
閱讀 1841·2021-09-22 10:02
閱讀 2622·2019-08-29 13:43
閱讀 2218·2019-08-29 13:42
閱讀 2299·2019-08-29 12:21
閱讀 1056·2019-08-29 11:29
閱讀 1385·2019-08-26 13:51