前言
最近一段时间,要为一个手机终端app程序从零开始设计一整套http api,因为面向的用户很固定,一个新的移动端app。目前还是项目初期,自然要求一切快速、从简,实用性为主。
下面将逐一论述我们是如何设计http api,虽然相对大部分人而言,没有什么新意,但对我来说很新鲜的。避免忘却,趁着空闲尽快记录下来。
技术堆栈的选择
php嘛?团队内也没几个人熟悉。
java?好几年没有碰过了,那么复杂的凯发天生赢家一触即发官网的解决方案,再加上团队内也没什么人会 ……
团队使用过lua,基于openresty构建过tcp、http网关等,对lua nginx组合非常熟悉,能够快速的应用在线上环境。再说lua语法小巧、简单,一个新手半天就可以基本熟悉,马上开工。
看来,nginx lua是目前最为适合我们的了。
http api,需要充分利用http具体操作语义,来应对具体的业务操作方法。基于此,没有闭门造车,我们选择了 这么一个小巧的框架,用于辅助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,不太可能花费太多的精力去做它的运维工作。
最终选择了轻量级的 ,非常紧凑,开箱即用。
最佳做法是把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技术堆栈运行的还不错,希望能给有同样需要的同学提供一点点的参考价值 :))
当然没有一成不变的架构模型,随着业务的逐渐发展,后面相信会有很多的变动。但这是以后的事情了,谁知道呢,后面有空再次记录吧~