【附近的人】系列之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的生成方式以及使用方式上叨叨這麼多,其實就是想說明一個問題:沒有對和錯,方法多種多樣,你可以博採眾長,最終能用就行…(如果你們有什麼好的想法或者指出我的錯誤,請後台留言我會在下篇貼出來的)
未完待續…