一個AccessToken引起的思考

最近在作一個微信預定洗車的項目,其中有個功能是預定完成後給用戶發一個模板消息,發送模板消息須要AccessToken以及json格式的消息內容,接口以下。javascript

發送模板消息 html

接口調用請求說明java

http請求方式: POST
 
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

POST數聽說明web

POST數據示例以下:redis

{
       "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"
               }
       }
   }

返回碼說明json

在調用模板消息接口後,會返回JSON數據包。正常時的返回JSON數據包示例:segmentfault

{
       "errcode":0,
       "errmsg":"ok",
       "msgid":200228332
   }

我而同事已經寫過這個功能了,索性就直接拿來用了。可是在使用的過程當中,發現第一次能夠成功發送模板消息,第二次就返回 errcode 40001,token驗證失敗。api

關於微信AccessToken的介紹:安全

access_token是公衆號的全局惟一票據,公衆號調用各接口時都需使用access_token。開發者須要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前爲2個小時,需定時刷新,重複獲取將致使上次獲取的access_token失效。(注:獲取access_token接口的每日調用限額爲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並非只有一份的,系統爲每一個會話都建立一個單獨的session,最後調用getAccessToken的會話讓其餘會話的session中的access_token都失效了。

我決定動手把代碼修改了一下,由於access_token的有效時間是7200秒,當時想着也放在redis裏面好了,能夠利用redis的自動過時來保證access_token的有效性,可是項目中沒有使用redis,加進來也是大材小用了,最後想一想仍是放在了ServletContext裏面。

ServletContext,是一個全局的儲存信息的空間,服務器開始,其就存在,服務器關閉,其才釋放。request,一個用戶可有多個;session,一個用戶一個;而servletContext,全部用戶共用一個。因此,爲了節省空間,提升效率,ServletContext中,要放必須的、重要的、全部用戶須要共享的線程又是安全的一些信息。

因而就有了下面這段代碼(僞)

public String getAccessToken(){
    Map<String,Object> cacheMap = 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);
}

這樣看起來好像是比以前的代碼好了一點,不會爲沒一個會話都建立一個access_token,並且保證了時效性。但其實仍是存在一點問題的,假若有兩個線程同時調用了這一個方法,其中第一個線程進了if在調用getTokenFormWx()的時候由於網絡或者其餘緣由等在這裏了,第二個線程來了仍是進了if,而且成功的調用getTokenFormWx()返回了token給調用者處理業務邏輯,這時候第一個線程執行完畢,刷新了token,這樣就致使了第二個線程的token已經失效,在處理業務邏輯的時候必然失敗。

咱們有沒有辦法避免這個問題呢?固然是有的。

你想我直接使用synchronized好了,加在方法上,這樣就不會錯了。因而方法就變成了這樣

public synchronized String getAccessToken(){
    Map<String,Object> cacheMap = 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(){
    Map<String,Object> cacheMap = 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);
}

當第一個線程進了if以後,執行synchronized裏面的代碼,等待在了getTokenFormWx(),第二個線程也進了if,但因爲加了synchronized,因此會等待在那裏,等第一個線程處理完它才能執行,第一個線程執行完畢以後返回token去執行業務邏輯,第二個線程進入synchronized代碼塊,執行這裏面的if判斷,因爲第一個線程已經成功獲取token而且刷新了ServletContext中的cacheMap,條件已經不知足,因此第二個線程是沒法執行這個if裏面的代碼了,到此咱們就設計了一個線程安全的獲取access_token方案。

看樣子好像一切都ok了,可是在測試後仍是會出現同樣的問題。

我又仔細檢查了兩遍代碼,仍是沒有發現有問題的地方。找不到錯誤的地方,我決定開始試錯。

第一次,我把https://api.weixin.qq.com/cgi...改爲https://api.weixin.qq.com/cgi...

參數access_token放入post請求參數裏面,其餘參數放進request body裏面。

結果:第一次就返回了40001 access_token無效。

第二次,我把https://api.weixin.qq.com/cgi...改爲https://api.weixin.qq.com/cgi...

參數access_token放入post請求參數裏面並使用trim()去除空格,其餘參數放進request body裏面。

結果:第一次就返回了40001 access_token無效。

第三次,我把https://api.weixin.qq.com/cgi...

其餘參數放進request body裏面。

結果:一切ok。。。。

爲何會多了空格?我也很想知道,但因爲調試了過久時間,已經很晚了,而次日就是假期,因此我也就沒有深究了。

那爲何第二次和第三次都對ACCESS_TOKEN進行了去空格處理,爲何返回的結果卻不同呢?

這就得不得不說一下Http協議了,但這裏不須要講太多,因此咱們只說一下Http協議之請求消息Request。

客戶端發送一個HTTP請求到服務器的請求消息包括如下格式:

請求行(request line)、請求頭部(header)、空行和請求數據四個部分組成。

圖片描述

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

對比一下你發現了什麼?

get請求參數在url後面,使用?看成標誌,多個參數使用&分割 相似?a=1&b=2

post參數在請求頭部空一行的後面 相似 a=1&b=2

那post提交的json串在哪一個位置呢?

其實你已經知道啦,也是在請求頭部空一行的後面 不過是以json的格式,而服務器內部使用&分割參數,使得開發者可使用getParameter獲取提交的參數,而其餘類型的參數(例如json串和xml)開發者可使用getInputStream來讀取到參數而後本身解析。

那post請求可否把參數寫在url後面呢?就像 post?a=1&b=2

答案是能夠的,服務器能夠成功解析到。

那get請求能把參數寫在request body裏面嗎?

答案是否認的,服務器對get請求只解析url後面的,request body裏面的他不關心。

那你發送模板消息的參數爲何寫在request body裏面就不行呢?

我也不知道微信內部是怎麼作的,可是我以爲吧,微信之因此要把access_token寫在url後面,由於這個接口request body裏面是模板消息的json串 若是再把access_token加進去 數據大概會是這樣

access_toke=xxxxxxxxxxx {"touser":"OPENID","template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", "url":"http://weixin.qq.com/download", ... }

微信方面也很差分割這個串,因而他們以爲要這個access_token寫在url後面,他們獲取到url後再手動分割處理,request body裏面就只放純json串,解析起來也很方便。這就是爲何我第二次操做失敗的緣由啦。

第一次寫技術類得文章,文筆很差多多見諒。

相關文章
相關標籤/搜索