聂永的博客

记录工作/学习的点点滴滴。

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 阅读(2357) 评论(0)  编辑  收藏 所属分类: HTTP移动后端


只有注册用户登录后才能发表评论。


网站导航:
 

公告

所有文章皆为原创,若转载请标明出处,谢谢~

新浪微博,欢迎关注:

导航

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

统计

常用链接

留言簿(58)

随笔分类(130)

随笔档案(151)

个人收藏

最新随笔

搜索

最新评论

阅读排行榜

评论排行榜