【附近的人】系列之API安全(一)

  • 2019 年 11 月 14 日
  • 筆記

我是老李,大家好,眾所周知【附近的人】系列有一段時間沒有更新了,搞得好像太監了似的,然而並沒有…這一系列只是周期不太穩定、不太調而已,原因說來倒也簡單:

  • 一來是畢竟我有我自己的安排(主要是懶)
  • 二來畢竟是免費的(不收錢,說話就是硬氣!)

當然了,上述兩條都是扯,主要是我想告訴你們的是:自己的事情一旦想好了,節奏自己一定要自己把握,你可以聽取別人的意見,但絕不能被其他影響到自己節奏把控甚至最後改變想法。其實,我還是很勤奮的,不信看github,不能說每天一點綠吧,至少一周還是有兩三點綠的…

渦輪增壓短期猛搞乃一時躁,大排自吸長期持久為永恆王道

其實很久(大概六個月)之前,本篇標題里的內容我就簡單討論過一些關於API安全的內容。只不過,畢時過境遷、如白駒過隙、任時光荏苒、怎物是人非,任由歲月在我稚嫩的臉龐上留下時光的痕迹,自那瑣碎如捧在手裡的水從間隙中流走,就在這個時候一旁邊打遊戲邊撇我寫文章的永強說[ 你TM這兩天看<情感讀書會>看多了吧,給人投稿呢? ]聽到他的這句話後的我立馬停止了這種文風回歸到以前,(承接上文)如今正好再來一波兒放到【附近的人】服務系列文章中由其自成一章,並與尼古拉斯永強的安全系列文章環環相扣,想必定能承上啟下、承前啟後、畫龍點睛[ 就在這個時候永強又撇了過來說了一句:你TM這是在你們公司和他們玩成語接龍玩石樂志么? ]於是我就停了不再繼續寫成語了…

首先,在山人看來,API安全主要是指兩個方面:

  • 一來是API本身儘可能低概率的被濫用:這裡主要是指被人給扒褲衩了惡意調用了,curl隨便搞,腳本嘻唰唰,甚至第三方客戶端
  • 二來是API和端飛數據儘可能低概率被其他人窺探到:這裡主要是說飛過來的參數抑或飛回去的處理結果,能加密最好加密
  • 三是防刷限流,不過我打算把限流單獨拿出去說下

請注意,前兩條里我都用到了【儘可能低概率】這幾個字,而不是類似於【三招教你杜絕xxx】的口吻。

在正式開始之前,我覺得我們還是有必要聊一聊token。API年代,不再像以前MVC套模板那種萬金油套路了,講究的是前後端分離,不分不高端,不分不舒服斯基。

只不過PC年代用戶登錄後,用戶的登錄憑證一般都是利用session/cookie(這裡把session和cookie放到一起了,因為絕大多數情況下session需要依賴cookie得以實現)這種方式,因為客戶端大多都是瀏覽器,而session/cookie可以說是為瀏覽器而生;而到了移動互聯網年代,一般都是前後分離的純API,在C/S里還要繼續使用cookie承載用戶登錄憑證不太合適,這會兒大家都會用token來代替session/cookie…當然了,非要在APP端里用cookie/session。。。。。。(沒太搞明白session機制的同學有興趣可以看下本次推送的第二篇文章或者鏈接https://t.ti-node.com/thread/6445811931751120897)


先聊聊TOKEN

我覺得在此有必要先聊一聊token的常規使用流程,其實token本質上就是我們又重新發明了一個自定義版的session,這句話你們好好琢磨琢磨。當前市面上,由於JWT的誕生出現,使得幾乎在所有討論有關token的地方都會自動分成兩大派:

  • JWT,token壓根不用單獨存儲處理的,直接用,不香嗎?
  • token和用戶信息(主要是uid)自己生成token,以kv方式存儲到kv介質中去,對token掌控更多,總之就是自己發明的野路子

這兩派人士往往會在出現token這種技術方案的任何場所中進行友好的磋商和探討,使得智慧的火花在碰撞中不斷產生:

正如上圖所示:討論將會在一篇祥和的氛圍中結束,雙方一起總結過去token方案的成就與不足,展望未來一年token方案的方向和目標!

所以,我就乾脆兩派的方案都尬聊一下得了…

先說下JWT的簡單構成:header + body(專業說法叫payload) + (header+body的消息認證碼),按理說就是這麼簡單,實際上也可能真的就是這麼簡單。只不過關於消息認證碼,這個我非常非常強烈建議你們抽空看下《我趙永強又回來了:單散、認證與數簽(五上)》,永強在這篇里用非常粗暴方式快速介紹了消息認證碼,這裡我就不在當復讀機了。

根據jwt的構成原理,我這兒用PHP碼了一坨簡單的demo代碼,你們先複製粘體走感受下:

<?php  $hash_alg = 'sha256';  $password = 'wahaha';  function jwt_encode( $data ) {    return rtrim( strtr( base64_encode( $data ), '+/', '-_' ), '=' );  }  function jwt_decode( $data ) {    return base64_decode( str_pad( strtr( $data, '-_', '+/' ), strlen($data) % 4, '=', STR_PAD_RIGHT ) );  }  /*      ------------ JWT header ----------------   */  $jwt_header_arr = array(    'alg' => $hash_alg,    'typ' => 'JWT',  );  /*      ------------ JWT body ----------------      其中,前七個都是jwt官網規定必須規範字段      最後一個uid是我自己搞的撒...   */  $jwt_body_arr = array(    'iss' => 'admin',    'exp' => time() + 600,    'sub' => 'test',    'aud' => 'every',    'nbf' => time(),    'iat' => time(),    'jti' => 10001,    'uid' => 32142353484,  );  $jwt_header = jwt_encode( json_encode( $jwt_header_arr ) );  $jwt_body   = jwt_encode( json_encode( $jwt_body_arr ) );  // 利用hash_hmac計算出jwt_header與jwt_body的消息認證碼  $jwt_sign   = hash_hmac( $hash_alg, $jwt_header.'.'.$jwt_body, $password );  $jwt_token  = $jwt_header.'.'.$jwt_body.'.'.$jwt_sign;  echo "下面是生成的jwt_token".PHP_EOL;  echo $jwt_token.PHP_EOL;  echo "下面解析並校驗jwt_token".PHP_EOL;  $jwt_token_arr = explode( '.', $jwt_token );  $jwt_body_json = jwt_decode( $jwt_token_arr[ 1 ] );  echo $jwt_body_json.PHP_EOL;

運行一下出了個結果,你們感受下,感覺和jwt挺相似的,應該是成功了:

所以用的時候咋用?就按照上述代碼相反的算法來一波兒就行,確切說就是先base64_decode一下,然後在json_decode,但是這就可以了?當然不是,因為還需要利用最後的消息認證碼來對數據完整性進行確認(注意上述demo代碼里並沒有實現對數據完整性進行校驗的代碼,你們自己補一下吧…),其實你可以粗暴理解為【驗簽】~

用法就是:

  • 客戶端通過用戶名密碼登錄,獲取到服務端生成的jwt
  • 客戶端把jwt存起來,訪問API的時候帶過去,一般說來不成文的規定就是放到http header中去,當然你扔到http body體甚至query string里都沒人管你
  • 服務端收到請求後,取出jwt,然後decode並校驗jwt數據完整性,然後從jwt_body里可以拿到uid或者是你們規定的其他信息,順利識別出用戶是誰

聽起來好像還不賴,反正能用…那麼,在此之前或者說與jwt並不完全一樣的token思路,比如有這麼實現的:就是利用AES加密(對AES加密需要簡單了解下的同學請在公眾號中摳「加密」兩個字)。我寫個簡單的demo你們複製粘貼走感受下,主要是思路(請務必仔細看注釋),你不一定也非要用imei和uid,你可以用任何【非敏感】數據來實現這個大致思路:

<?php  /*    (imei和token都假設在http header中)    思路就是拿設備imei當作aes加密密鑰,給uid+imei進行加密,結果當token    用戶登錄成功後,把這個token返回給客戶端    客戶端保存好token,訪問接下來api帶上這個token以及imei    API收到token後,利用imei進行解密,如果【能解密成功並且解密後的imei等於    API訪問提交上來imei】注意這裡要當兩個條件同時並列成立,就算是有效token  */  // 採集到的設備id  $imei = 'MD101C4MX30I95ZX6A';  // 用戶uid  $uid = 4123456767;  // aes加密採用具體分組算法  $aes_method = 'aes-256-cbc';  // 密鑰  $key  = $imei;  $data = $imei.$uid;  // aes-128-cbc分組加密算法需要的iv向量的長度  $iv_length = openssl_cipher_iv_length( $aes_method );  // 根據長度生成相應iv  $iv = openssl_random_pseudo_bytes( $iv_length, $cstrong );  echo "明文:".$imei.$uid.PHP_EOL;  $enc_data = base64_encode( openssl_encrypt( $data, $aes_method, $key, OPENSSL_RAW_DATA, $iv ) );  echo "秘文:".$enc_data.PHP_EOL;  $dec_data = openssl_decrypt( base64_decode( $enc_data ), $aes_method, $key, OPENSSL_RAW_DATA, $iv );  echo "解密:".$dec_data.PHP_EOL;    // 注意!⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️  // 我偷懶了,依然沒有寫校驗部分,很簡單你們自己補充下哈...

保存一下,運行結果就是這樣shai兒:

這個思路和jwt不一樣的就是:jwt的payload部分實際本質上是明文,一次base64-decode你就可以看到明文是什麼,所以你一定要通過hash_hmac系列函數來對中間的payload進行數據完整性校驗,從而保證payload被篡改後立馬就會無效;而加密的方案相當於對payload進行aes加密,如果payload被篡改,解密會失敗且校驗無法通過,如果說token泄露被copy走了,那麼由於imei發生變化,也會導致token校驗失敗從而杜絕信息泄露,除非這傢伙猜到了我們用imei參與了token運算。

除此之外,還有一些方案就是將token和用戶信息以[ key : value ]的方式存入到redis或memcache中去,API收到token後會從redis或memcache中獲取到用戶信息…

在token的生成方式以及使用方式上叨叨這麼多,其實就是想說明一個問題:沒有對和錯,方法多種多樣,你可以博採眾長,最終能用就行…(如果你們有什麼好的想法或者指出我的錯誤,請後台留言我會在下篇貼出來的)

未完待續…