聶永的博客

          記錄工作/學習的點點滴滴。

          HTTP API設計筆記

          前言

          最近一段時間,要為一個手機終端APP程序從零開始設計一整套HTTP API,因為面向的用戶很固定,一個新的移動端APP。目前還是項目初期,自然要求一切快速、從簡,實用性為主。

          下面將逐一論述我們是如何設計HTTP API,雖然相對大部分人而言,沒有什么新意,但對我來說很新鮮的。避免忘卻,趁著空閑盡快記錄下來。

          技術堆棧的選擇

          PHP嘛?團隊內也沒幾個人熟悉。

          Java?好幾年沒有碰過了,那么復雜的解決方案,再加上團隊內也沒什么人會 ……

          團隊使用過Lua,基于OpenResty構建過TCP、HTTP網關等,對Lua + Nginx組合非常熟悉,能夠快速的應用在線上環境。再說Lua語法小巧、簡單,一個新手半天就可以基本熟悉,馬上開工。

          看來,Nginx + Lua是目前最為適合我們的了。

          HTTP API,需要充分利用HTTP具體操作語義,來應對具體的業務操作方法。基于此,沒有閉門造車,我們選擇了 http://lor.sumory.com/ 這么一個小巧的框架,用于輔助HTTP API的開發開發。

          嗯,OpenResty + Lua + Lor,就構成了我們簡單技術堆棧。

          HTTP API簡要設計

          HTTP API路徑和語義

          每一具體業務邏輯,直接在URL Path中體現出來。我們要的是簡單快速,數據結構之間的連接關系,盡可能的去淡化。eg:

          /resource/video/ID
          

          比如用戶反饋這一模塊,將使用下面比較固定的路徑:

          /user/feedback
          
          • GET,以用戶維度查詢反饋的歷史列表,可分頁
            • curl -X GET http://localhost/user/feedback?page=1
          • POST,提交一個反饋
            • curl -X POST http://localhost/user/feedback -d "content=hello"
          • DELETE,刪除一個或多個反饋,參數附加在URL路徑中。
            • curl -X DELETE http://localhost/user/feedback?id=1001
          • PUT,更新評論內容
            • curl -X PUT http://localhost/user/feedback/1234 -d "content=hello2"

          用戶屬性很多,用戶昵稱只是其中一個部分,因此更新昵稱這一行為,HTTP的 PATCH 方法可更精準的描述部分數據更新的業務需求:

          /user/nickname
          
          • PATCH,更新用戶昵稱,昵稱是用戶屬性之一,可以使用更輕量級的 PATCH 語義
            • curl -X PATCH http://localhost/user/nickname -d "nickname=hello2"

          嗯,同一類的資源URL雖然固定了,但HTTP Method呈現了不同的業務邏輯需求。

          HTTP API的訪問授權

          實際業務HTTP API的訪問是需要授權的。

          傳統的Access Token解決方案,有session回話機制,一般需要結合Web瀏覽器,需要寫入到Cookie中,或生產一個JSessionID用于標識等。這針對單純面向移動終端的HTTP API后端來講,并沒有義務去做這一的兼容,略顯冗余。

          另外就是 OAUTH 認證了,有整套的認證方案并已工業化,很是成熟了,但對我們而言還是太重,不太適合輕量級的HTTP API,不太可能花費太多的精力去做它的運維工作。

          最終選擇了輕量級的 Json Web Token,非常緊湊,開箱即用。

          最佳做法是把JWT Token放在HTTP請求頭部中,不至于和其它參數混淆:

          curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2NyIsInV0eXBlIjoxfQ.LjkZYriurTqIpHSMvojNZZ60J0SZHpqN3TNQeEMSPO8" -X GET http://localhost/user/info
          

          下面是一副瀏覽器段的一般認證流程,這與HTTP API認證大體一致:

          JWT的Lua實現,推薦: https://github.com/SkyLothar/lua-resty-jwt.git,簡單夠用。

          JWT和Lor的結合

          jwt需要和業務進行綁定,結合 lor 這個API開發框架提供的中間件機制,可在業務處理之前,在合適位置進行權限攔截。

          • 用戶需要請求進行授權接口,比如登陸等
          • 服務器端會把用戶標識符,比如用戶id等,存入JWT的payload負荷中,然后生成Token字符串,發給客戶端
          • 客戶端收到JWT生成的Token字符串,在后續的請求中需要附加在HTTP請求的Header中
          • 完成認證過程

          不同于OAUTH,JWT協議的自包含特性,決定了后端可以將很多屬性信息存放在payload負荷中,其token生成之后后端可以不用存儲;下次客戶端發送請求時會發送給服務器端,后端獲取之后,直接驗證即可,驗證通過,可以直接讀取原先保存其中的所有屬性。

          下面梳理一下Jwt認證和Lor的結合。

          • 全局攔截,針對所有PATH,所有HTTP Method,這里處理JWT認證,若認證成功,會直接把用戶id注入到當前業務處理上下文中,后面的業務可以直接讀取當前用戶的id值
          app:use(function(req, res, next)
              local token = ngx.req.get_headers()["Authorization"]
              -- 校驗失敗,err為錯誤代碼,比如 400
              local payload, err = verify_jwt(token)
              if err then
                  res:status(err):send("bad access token reqeust")
                  return
              end
          
              -- 注入進當前上下文中,避免每次從token中獲取
              req.params.uid = payload.uid
          
              next()
          end)
          
          • 針對具體路徑進行設定權限攔截,較粗粒度;比如 /user 只允許已登陸授權用戶訪問
          app:use("/user", function(req, res, next)
              if not req.params.uid then
                  -- 注意,這里沒有調用next()方法,請求到這里就截止了,不在匹配后面的路由
                  res:status(403):send("not allowed reqeust")
              else
                  next() -- 滿足以上條件,那么繼續匹配下一個路由
              end
          end)
          
          • 一種是較細粒度,具體到每一個API接口,因為雖然URL一致,但不同的HTTP Method有時請求權限還是有區別的
          local function check_token(req, res, next)
              if not req.params.uid then
                  res:status(403):send("not allowed reqeust")
              else
                  next()
              end
          end
          
          local function check_master(req, res, next)
              if not req.params.uid ~= master_uid then
                  res:status(403):send("not allowed reqeust")
              else
                  next()
              end
          end
          
          local lor = require("lor.index")
          local app = lor()
          
          -- 聲明一個group router
          local user_router = lor:Router()
          
          -- 假設查看是不需要用戶權限的
          user_router:get("/feedback", function(req, res, next)
          end)
          
          user_router:put("/feedback", check_token, function(req, res, next)
          end)
          
          user_router:post("/feedback", check_token, function(req, res, next)
          end)
          
          -- 只有管理員才有權限刪除
          user_router:delete("/feedback", check_master, function(req, res, next)
          end)
          
          -- 以middleware的形式將該group router加載進來
          app:use("/user", user_router())
          
          ......
          
          app:run()
          

          為什么沒有選擇GraphQL API ?

          我們在上一個項目中對外提供了GraphQL API,其(在測試環境下)自身提供文檔輸出自托管機制,再結合方便的調試客戶端,確實讓后端開發和前端APP開發大大降低了頻繁交流的頻率,節省了若干流量,但前期還是需要較多的培訓投入。

          但在新項目中,一度想提供GraphQL API,遇到的問題如下:

          • 全新的項目數據結構屬性變動太頻繁
          • 普遍求快,業務模型快速開發、調試
          • 大家普遍對GraphQL API有些抵觸,使用JSON輸出格式的HTTP API是約定俗成的習慣選擇

          毫無疑問,以最低成本快速構建較為完整的APP功能,HTTP API + JSON格式是最為舒服的選擇。

          雖然有些擔心服務器端的輸出,很多時候還是會浪費掉一些流量,客戶端并不能夠有效的利用返回數據的所有字段屬性。但和進度以及人們已經習慣的HTTP API調用方式相比,又微乎其微了。

          小結

          當前這一套HTTP API技術堆棧運行的還不錯,希望能給有同樣需要的同學提供一點點的參考價值 :))

          當然沒有一成不變的架構模型,隨著業務的逐漸發展,后面相信會有很多的變動。但這是以后的事情了,誰知道呢,后面有空再次記錄吧~

          posted on 2018-01-02 20:53 nieyong 閱讀(2440) 評論(0)  編輯  收藏 所屬分類: HTTP移動后端

          公告

          所有文章皆為原創,若轉載請標明出處,謝謝~

          新浪微博,歡迎關注:

          導航

          <2018年1月>
          31123456
          78910111213
          14151617181920
          21222324252627
          28293031123
          45678910

          統計

          常用鏈接

          留言簿(58)

          隨筆分類(130)

          隨筆檔案(151)

          個人收藏

          最新隨筆

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 隆安县| 平南县| 宜都市| 扎兰屯市| 隆化县| 措美县| 苏尼特左旗| 土默特左旗| 青河县| 连州市| 合作市| 塔河县| 巨野县| 息烽县| 眉山市| 黄平县| 贺州市| 甘孜县| 阳春市| 桂林市| 日照市| 呼和浩特市| 醴陵市| 永康市| 鄂托克旗| 永昌县| 仁寿县| 奉贤区| 台湾省| 博爱县| 阿合奇县| 米林县| 辽宁省| 茶陵县| 宁河县| 广水市| 兴义市| 刚察县| 高邮市| 池州市| 蒙阴县|