App项目实战之路(二):API篇

概述篇发布出去后,收到很多人的大力支持,也收到了几点关于功能需求的建议,主要在于几点:

  1. 只有微信登录在App Store那边审核很可能通不过;
  2. 调用微信获取用户头像和昵称的接口需要企业微信号才行;
  3. 就算微信登录也存在需要修改头像和昵称的需求。

关于第1点,细想一下就知道,只有第三方账号登录的确是通不过审核的。因为提交审核时必须提供测试账号给App Store的审核人员。审核人员是不会使用自己的账号进行测试的,不管是自己的微信、微博还是手机号。之前我是掉过这个坑的,提交了一款以手机号+短信验证码登录的App,但没有提供测试账号,结果被打回来了。所以,还是需要建立自己的用户体系,这一点无法偷懒了。

关于第2点,则是因为微信对这部分接口做了权限控制,只有通过了开发者资质认证才有权开通此接口。但微信的开发者资质认证并不支持个人开发者。另外,还要交每年300元的审核费用。其实,未认证的开发者建立的App只有分享的权限,根本没有登录的权限。所以,微信登录这条路根本通不了。因此,我决定不用微信登录了,改用Github登录。毕竟,面向的用户群是程序猿,而程序猿基本是人手一个Github账户。没有Github账户的,称不上合格的程序猿。也不考虑再加入微博、QQ、Facebook、Twitter等社交账户登录。因为选择太多容易混乱,我自己在某些平台登录时,就经常不记得上一次是用哪个账户登录的。

关于第3点,毫无疑问,修改头像和昵称的功能需要保留。

因此,最终的功能需求应该如下:

  1. 手机号 + 短信验证码注册
  2. 手机号 + 短信验证码登录
  3. Github登录
  4. 上传图片
  5. 修改头像
  6. 修改昵称
  7. 设置用户技术栈标签
  8. 获取同栈之猿的内容列表
  9. 获取关注之猿的内容列表
  10. 获取同栈的用户列表(未有关注之猿时获取)
  11. 发布问题
  12. 发布分享
  13. 关注某条内容
  14. 取消关注内容
  15. 获取内容的评论列表
  16. 添加评论
  17. 回复评论
  18. 点赞评论
  19. 关注某用户
  20. 取消关注某用户
  21. 获取某人详细资料
  22. 获取某人的发布内容
  23. 获取某人关注的人
  24. 获取某人的粉丝列表
  25. 获取我的消息
  26. 提交意见反馈
  27. 退出登录

需求确定,接着就可以开始设计API了。

REST API

关于什么是REST,我就不在这里赘述了,直接推荐REST作者的经典论文:

下面我只想用一些实例描述几种架构风格在API定义方面的不同。

假如现在要定义登录、退出登录、注册、查询用户资料的接口,那么,可以这样定义:

接口

方法

Endpoint

登录

POST

/user/login

退出登录

POST

/user/logout

注册

POST

/user/register

查询用户资料

GET

/user/queryInfo

使用这种风格的貌似很多。也有些不是在URI中定义接口,而在参数中用method或action之类的参数名区分不同接口,示例如下:

接口

方法

参数

登录

POST

method=login

退出登录

POST

method=logout

注册

POST

method=register

查询用户资料

GET

method=queryUserInfo

最后,再看下面这种接口的定义:

接口

方法

Endpoint

登录

POST

/sessions

退出登录

DELETE

/sessions/{session_id}

注册

POST

/users

查询用户资料

GET

/users/{user_id}

这三种定义有什么区别呢?其实,前面两种可以认为都是 RPC(Remote Procedure Call) 风格的,而最后这种则可认为是 REST 风格的。

RPC和REST区别在哪呢?

最直接的区别就是:RPC抽象的是过程,REST抽象的是资源。过程是以动词为核心,而资源是以名词为核心。也可以简单类比为:RPC是面向过程的,REST是面向对象的。

从上面的例子就可以看出,前面两种定义,每个接口分别用了一个操作性的词语去定义;而最后一种定义,登录和退出登录都属于 /session 资源,注册和查询用户资料都属于 /user 资源,然后分别用POST、DELETE、GET等方法对同个资源定义不同操作。

我发现,还有些定义是RPC-REST混合的,例如,可能会这样子定义:

接口

方法

Endpoint

登录

POST

/users/login

退出登录

POST

/users/logout

注册

POST

/users/register

查询用户资料

GET

/users/{user_id}

如果再加个修改用户资料的接口,可能是这样子的:

接口

方法

Endpoint

修改用户资料

POST

/users/{user_id}/update

给我的感觉就是:好混乱!这种大部分都是在对REST有过很初浅的了解,但却缺少正确理解的情况下做出的设计。或者是对于部分接口不知道该如何抽象为资源,所以就直接用RPC方式去定义了。

其实,使用REST风格设计API,我觉得难点就在于如何抽象资源。使用RPC则相对容易很多。这时,也许有人就会提出疑问了。既然使用RPC比用REST更容易抽象出接口,那为何还要用REST呢?要解答这个疑问,可以从面向过程和面向对象的角度去思考。我们知道,面向过程的思考方式处理问题更直接简单,那为什么我们还要使用面向对象呢?至于这个问题的答案,我就不再展开了。

API定义

本项目的API是打算使用REST方式定义的。那么,首先,就是资源的Endpoint定义。根据前面的功能需求整理出以下资源,可能会有些遗漏:

Endpoint

资源

/files

文件

/files/{file_id}

某个文件

/sessions

会话

/sessions/{session_id}

某个会话

/users

用户

/users/{user_id}

某用户

/users/{user_id}/posts

某用户发布的内容

/users/{user_id}/following

某用户关注的人

/users/{user_id}/followers

某用户的粉丝

/posts

发布的内容

/posts/{post_id}

某条内容

/posts/{post_id}/comments

某条内容的评论

/me

当前用户

/me/posts

我发布的内容

/me/stars

我星标的内容

/me/following

我关注的人

/me/followers

我的粉丝

/me/messages

我的消息

定义资源的Endpoint时,需要分清楚不同资源的层级关系。一个定义良好的URI,应该具有可读性,即从URI本身即可知道它所代表的资源。另外,对于URI中的一些变量值,如{file_id}、{session_id}、{user_id}、{post_id}等,在传值的时候必须确保不能为空,可以设置默认值。

接着,就需要对每个资源定义操作的方法了。我倾向于使用以下四个方法:

方法

描述

示例

示例说明

POST

创建新资源

/posts

创建新内容

GET

查询资源

/posts

查询内容列表

PUT

修改资源

/posts/{post_id}

修改某条内容

DELETE

删除资源

/posts/{post_id}

删除某条内容

不过,并不是所有资源都会开放这四个方法。例如,对/post是不开放PUT和DELETE方法的。对于以上资源,具体需要定义哪些方法,这里就不再列出来了。

然后,还要加入版本控制。毕竟,接口不是一成不变的,需要不断改动升级版本应对各种变化。那么,版本号要加在哪里好呢?关于这个问题,网上有很多讨论,有些人喜欢直接加在URI中,像这样:

http://api.domain.com/v2.1/posts

有些人喜欢加在参数里,像这样:

http://api.domain.com/posts?version=2.1

也有些人喜欢加在Header里,像这样:

Accept: application/json;version=2.1

或者自定义Header

api-version: 2.1

不喜欢第一种方式的人,大部分理由是,URI表示资源,应该与版本无关。而第二种方式和第一种方式本质上是一样的。大部分人建议使用第三种方式。不过,发现好多开放API都是采用第一种方式。在我看来,加在哪里其实影响不大。在本项目中,我打算和大部分开放API一样采用第一种方式即可。另外,如果版本号不提供,则默认为采用最新版本的接口。

最后,再定义下响应的数据协议。初期打算使用JSON,后期可能会考虑使用Protocol Buffers。数据结构则如下:

{
code:200,
message: "success",
data: { key1: value1, key2: value2, ... }
}
  • code: 错误码
  • message: 描述信息,成功时为”success”,错误时则是错误信息
  • data: 成功时返回的数据,类型为对象或数组

之前,我是喜欢将请求状态码和业务错误码分开处理的。因此,这里的code我之前喜欢将其定义为业务错误码。但是,如果按照REST风格来设计,还是有统一的code更合适。因此,我这次尝试下改变习惯。

API安全设计

安全设计方面,首先,我打算全面使用HTTPS。使用HTTPS,虽然牺牲了性能,但可以解决大部分安全问题。另外,苹果在之前的WWDC上就已宣布,从2017年1月1日起,所有iOS应用将强制使用HTTPS。这其实也意味着,从2017年起,所有App都将会使用HTTPS,不只是iOS。除非有个别比较奇葩,非要搞HTTP和HTTPS两套。至于HTTPS的优化,则需要慢慢搞了。至于证书,自己弄个自签名证书即可。后期需要支持Web版的话再找个靠谱的CA注册证书。

其次,用户鉴权方面则打算采用Token方式。用户登录之后分配一个accessToken和一个refreshToken,accessToken用于发起用户请求,refreshToken用于更新accessToken。accessToken会设置有效期,可以设为24小时。而用户退出登录之后,accessToken和refreshToken都将作废。重新登录之后会分配新的accessToken和refreshToken。

然后,我还打算在App层级分配AppKeyAppSecret,Android和iOS分别分配一对。每次向服务端发送请求时,AppKey都必须带上,服务端会对相应的AppKey进行校验。而AppSecret则需要安全保存在客户端,也不能在网络上进行传输,防止泄露。AppSecret只用于加密一些安全性级别较高的数据,以及为URL生成签名。URL签名算法步骤如下:

  1. 将所有参数按参数名进行升序排序;
  2. 将排序后的参数名和值拼接成字符串stringParams,格式:key1value1key2value2…;
  3. 在上一步的字符串前面拼接上请求URI的Endpoint,字符串后面拼接上AppSecret,即:stringURI + stringParams + AppSecret;
  4. 使用AppSecret为密钥,对上一步的结果字符串使用HMAC算法计算MAC值,这个MAC值就是签名。

另外,如果为了再加强安全性,参与签名的参数列表中可以再添加个timestamp字段,值为发送请求时的时间戳,每次请求的时间戳都将不同,这样不止增加了签名的不可预测性,也可以防止重放攻击。服务端收到请求后先检查时间戳离当前时间是否过久,如果过久则不予处理。不过,这还涉及到客户端和服务端时间同步的问题。这个很难保持一致,就算使用长连接不断获取服务器时间,也会因为网络原因而存在延迟,而且在移动网络延迟可能还会比较高。

还有另一种方案,就是使用nonce字段,值为一个较长的随机数,而不是时间戳。每次请求的随机数也都会不同,可以达到同样的效果。不过,采用这种方案的话,那服务器需要保存以前发送过的nonce。每次收到请求后先检查nonce是否已存在,存在则不予处理。这样,时间久了,nonce的量将会非常大。也有一种优化方案,那就是每次请求的nonce值由服务端生成并发送给客户端。即是说,客户端每次发送正式请求之前,需要先向服务端请求nonce值。这样的话,服务端则可以在有请求过来的时候才生成nonce,请求处理完之后则可以删除nonce。不过,弊端也很明显,本来一次的请求变成了两次。

不过,在我的这个项目中,初期我只要求加强签名的不可预测性即可,而nonce方案具备更高的不可预测性。因此,我将采用的方案是:在客户端自己生成nonce,但服务端不保存nonce,只要检查请求中是否存在nonce即可。

URL签名在每次发送请求时都需要附加在参数中,服务端接收到请求后会使用同样的签名算法计算签名值,只有服务端计算出来的签名值和接收到的签名值一致时才认为请求是安全的。

写在最后

自此,API部分的设计就完成了。在此总结一下:

  1. 采用REST风格定义API,接口抽象成对资源的操作;
  2. 添加API版本控制,版本号嵌在URL中;
  3. 响应统一使用code、message、data的JSON数据格式;
  4. 全站采用HTTPS;
  5. 使用Token方式对用户鉴权;
  6. 使用AppKey方式对应用鉴权;
  7. 使用URL签名对请求鉴权;
  8. 参数中添加nonce值增强签名的不可预测性。

原文发布于微信公众号 - Keegan小钢(keeganlee_me)

原文发表时间:2016-08-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CodingToDie

分布式事务解决方案

Spring Cloud 分布式事务管理 在微服务如火如荼的情况下,越来越多的项目开始尝试改造成微服务架构,微服务即带来了项目开发的方便性,又提高了运维难度以及...

5945
来自专栏风火数据

高级Java研发师在解决大数据问题上的一些技巧

众所周知, Java 在处理数据量比较大的时候,加载到内存必然会导致内存溢出,而在一些数据处理中我们不得不去处理海量数据,在做数据处理中,我们常见的手段是分解,...

1302
来自专栏云计算教程系列

如何在CentOS上安装Mosquitto服务器

MQTT(消息队列遥测传输)是ISO 标准下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设...

5190
来自专栏云计算教程系列

如何在Ubuntu 18.04上安装和保护Mosquitto MQTT Messaging Broker

MQTT是一种机器到机器的消息传递协议,旨在为“物联网”设备提供轻量级的发布/订阅通信。它通常用于地理跟踪车队,家庭自动化,环境传感器网络和公用事业规模数据收集...

9051
来自专栏喔家ArchiSelf

老曹眼中的Linux基础

Linux 几乎无处不在,不论是服务器构建,还是客户端开发,对操作系统的基本理解和基础技能的掌握对全栈来说都是必备的。

1273
来自专栏IT技术精选文摘

基于Netty实现海量接入的推送服务技术要点

2175
来自专栏安恒信息

网络威胁预警 Flash跨域数据劫持漏洞

目前互联网上许多网站都允许普通用户上传文件,但是很多网站在开发设计过程中由于程序员缺乏安全意识,逻辑处理方面不够严谨,导致在用户上传文件的过程中没有...

3005
来自专栏FreeBuf

Apache已修复Apache Tomcat中的高危漏洞

近日,Apache软件基金会为Tomcat应用程序服务器推送了最新的安全更新,并修复了多个安全漏洞,其中包括一个DoS漏洞和一个信息泄露漏洞。

1805
来自专栏前端黑板报

HTTP2基础教程-读书笔记(三)

前面两篇记录了HTTP的历史和网络请求、页面渲染的过程以及HTTP/1的一些问题,本篇就来讲一下迁移HTTP/2需要考虑的一些问题。 迁移HTTP/2说简单无非...

3589
来自专栏IT大咖说

送给前端的你:可视化快速生成模拟数据服务——Easy Mock

? 内容来源:2017年11月18日,大搜车前端工程师高攀在“2017中国开源年会”进行《Easy Mock 接口数据模拟服务》演讲分享。IT 大咖说(微信i...

4906

扫码关注云+社区

领取腾讯云代金券