前言
最近一段时间,要为一个手机终端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技术堆栈运行的还不错,希望能给有同样需要的同学提供一点点的参考价值 :))
当然没有一成不变的架构模型,随着业务的逐渐发展,后面相信会有很多的变动。但这是以后的事情了,谁知道呢,后面有空再次记录吧~