嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

周梦康 发表于 2016-01-07 37430 次浏览 标签 : REST

REST是一种软件架构风格。RESTful Api 是基于 HTTP 协议的 Api,是无状态传输。它的核心是将所有的 Api 都理解为一个网络资源。将所有的客户端和服务器的状态转移(动作)封装到 HTTP 请求的 Method  之中。

详情可以阅读 http://mengkang.net/620.html 。

而本篇文章则主要是讨论 RESTful Api 身份认证安全性设计。

没有绝对的安全,这个话题很深,下文都是自己的一些理解,水平有限,如有勘误,希望大家予以指正。


由于 RESTful Api 是基于 Http 协议的 Api,是无状态传输,所以只要和用户身份有关的请求都会带上身份认证信息。(很多时候客户端事先并不知道某个 api 后期会不会加入身份判断,所以我们一般都会选择每个请求都会带上认证信息,如果有的话。)

Http Basic Authentication

Http Basic 是一种比较简单的身份认证方式。在 Http header 中添加键值对 Authorization:  Basic xxx (xxx 是 username:passowrd base64 值)。

例如 username 为 zmk ,password 为 123456,请求则如下

GET /auth/basic/ HTTP/1.1
Host: xxxxx
Authorization: Basic em1rOjEyMzQ1Ng==

Base64 的解码是非常方便的,如果不使用 Https ,相当于是帐号密码直接暴露在请求中。

危险性高,实际开发者使用的应该几乎为0。

顺便提下 DIGEST 认证,和 BASIC 认证相差无几,而且不适合 api 设计,实际又需要两次请求,首次请求,服务器端返回401,并且带上nonce值,然后客户端再利用username+password+nonce默认MD5之后再请求。对 http 请求的作用是仅仅防止二次请求,对身份认证并没有什么提升。

Access Token

不知道是否应该这么称呼。原理即当客户端登录完毕之后,给客户端返回一个token,服务器端控制该token的有效期,每次请求都带上该值,然后服务器端做验证,退出之后,客户端通知服务端端销毁token,客户端本地也销毁。但是如果抓包获取到token,就能任意伪造请求了。

同时 api 接口还存在被第三方开发者或者公司随意利用的风险。也就是说,别人可以非常轻易的就弄出一个你们 app 的复制版,而且还用的你们的所有资源。

危险性高,实际开发估计使用得还不少。

Api Key + Security Key + Sign

下图是我们自己每次请求的身份认证的方式,如有不足,请大家指出。可以说是 JWT 的自定义版吧。

RESTful Api 身份认证中的安全性设计探讨


这里的认证逻辑即:

  1. 用户登录返回一个api_keysecurity_key

  2. 然后客户端将security_key存在客户端;

  3. 当要发送请求之前,通过function2加密方法,把如图所示的五个值一起加密,得到一个sign

  4. 发送请求的时候,则将除去security_key之外的值,以及sign一起发送给服务器端;

  5. 服务器端首先验证时间戳是否有效,比如是服务器时间戳5分钟之前的请求视为无效;

  6. 然后根据api_key得到sercurity_key

  7. 最后验证sign

Api key 的作用是什么?(补)

看到有朋友在头条问了这个问题,说下我的实际使用场景:

api key 是用来标识每个不同用户的(也就是说 api key 和用户 id 一一对应的),同时也用来验证security_keysign的。

比如有2000万用户,以 redis 作为数据库,将api_key为键,security_key作为值,api_key散列分布(比如对末尾位字符的ASCII对20取模)到20个 hashes 里。

当用户请求过来的时候首先根据api_key找到对应的 hashes,首先HEXISTS检查该api_key是否存在,存在则通过HGET取出该值,最后一起验证sign

是否需要加上时间戳验证?

上面的认证逻辑中加密得到签名的时候,把时间戳加进去是为了在一定程度上屏蔽了一些无效的请求,可以略去,也可以设计的更加严格。如果想防止恶意的 api ddos 攻击,这一步验证肯定是不行的。需要做更多的验证,比如用户验证,ip 验证等。可以参考 github 的 api 的设计。它会在返回的 http 头信息里带上

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999

表示这个接口在某一时间段内,该授权用户调用该接口的最大次数为5000次,该时间段内还剩余4999次。当然,这样的验证加上之后,在代码的执行效率上肯定会有所影响。

是否需要将request_parameters也加入到sign生成的算法之中?

也不是必须的,仅仅是为了请求的真实性,减少请求的伪造,比如有人抓包拿到 http 请求之后,如果没有验证sign这步,那么别人就可以非常简单的修改请求的参数,而请求都会生效。

这里将request_parameters也加入到签名之中,就减少了伪造请求的可能性,但是无法杜绝,破坏者可能就非要黑你,又对逆向工程非常熟悉,找到我们加密算法的实现,依然可以未知出合法的签名,所以我们常说,服务器端永远不能相信客户端的请求都是安全的、合法的,需要做验证的都还是不能省略。

同时这(sign算法)也造成了 api 接口调试的成本,api 测试工具必须也得实现那一套算法,或者是设置在开发环境下不做验证。我们在配置开发环境的时候则是 vpn 连测试服务器所在内网,然后进行测试,否则开发环境也存在被人利用的风险。

项目实例 https://github.com/zhoumengkang/netty-restful-server

JWT

JWT (JSON Web Token) 使用流程如下(图片来自官网)

其认证机制也是登录,发放密钥给客户端,然后客户端每次发送请求的时候通过 JWT 的算法规则组装 JWT 的Auth Header,服务器端作验证。

web 授权认证的原理万变不离其宗,都是如此。

只不过 JWT 呢,自定了一套认证协议。格式为Header.Payload.Signature。比如xxxxx.yyyyy.zzzzz。签名内容是有Header+Payload+Secret通过HMAC SHA256算法加密而成。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

而请求的很多参数键值对都可以放在Payload里面。完整讲解请求看官方的介绍 http://jwt.io/introduction/

需要注意的一点,依照 JWT 的协议,只有一个secret,无法得知该用户是谁,所以在secret该值中必须要可以解码出用户的id。

而我们自定义认证协议的时候header感觉就没有必要了,使用什么算法事先定义好即可。所以我们也没选择这种方式而是上面的那种方式。

其他

oauth2.0 则属于第三方认证,不在本篇的讨论范畴之内,可以阅读 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

如果觉得这篇文章不错的话,可以分享到

嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

评论列表

回复 路人甲 2016-01-12 10:34:17
1、你的意思是每个用户都会分配security_key,然后存储在客户端,那和cookie session验证有什么区别(完全可以每次都随机生成session串,每次用户请求时验证session的合法性。这样还能去掉算sig的算法,提高效率)
2、如果是浏览器请求,你就要把你的sig算法写到js中,怎么保证你的js算法不泄露,如果泄露是不是相当于告诉别人你加密的方式
回复 康哥 2016-01-12 10:49:52
回复路人甲: 谢谢予以指导。
请教下,每次随机生成 session 串,是如何认证的呢?
这里做这些验证同时为了防止接口被其他人随意利用,而弄出一款仿制品。
您说的第二个问题,我们现在客户端和 web 端还不是一套 api,web 端的可以请求则可以不受上面的限制,XHR 请求的时候,只允许指定域名的请求跨域,拦截其他非法来源。
哪有说得不对的还望指正。
回复 路人甲 2016-01-12 14:21:36
弄个爬虫把你们的资源全部爬下来,就不存在跨域了。还有很多其他手段的。
要防止别人复制,最好的就是别公布api。
文章说了一大堆,最后总结竟然是为了防止仿制,完全跑题了。
回复 康哥 2016-01-12 15:43:06
回复路人甲: 批评的对,我那会追加的,反而显得更乱了。
回复 钟玉洪 2016-01-13 23:09:13
写的还是可以,写出来大家可以讨论。
回复 康哥 2016-01-14 10:40:28
回复钟玉洪: 谢谢鼓励。
回复 dyllen 2016-01-15 18:16:45
你的第二点Security Key的,我觉得和access token相差不大。
只是增加的伪造的难度。
access token是拿到直接可以伪造,
而Security Key需要一点功夫。
还有就是不需要认证的API一样可以伪造请求。
回复 康哥 2016-01-15 18:37:24
回复dyllen: 说得对,只是增加了伪造的难度。谢谢点评,不知道大家日常都是如何配置,闭门造车见笑了。
回复 dyllen 2016-01-15 18:55:58
回复康哥: 其实我也不知道怎么搞好!我是用的token的形式!我也是想看看别人是怎么做的。
回复 dyllen 2016-01-15 18:57:23
回复康哥: 你的评论框点击提交会打开一个空白的新标签页,点击评论的回复也是一样会弹出一个空白的新标签页。Firfox43。
回复 康哥 2016-01-15 21:55:54
回复dyllen: 哈哈,好的,我就是自己写的都,伪前段。谢谢提醒。
回复 愿与光同尘 2016-01-21 14:19:52
api_key是不是也存在了客户端了?
回复 康哥 2016-01-21 15:11:26
回复愿与光同尘: 嗯,是的,只是 api_key 在每次请求中都显性的显示,而 api_security 隐形传输(加密之后)。
回复 Eric 2016-03-11 15:09:52
看了JWT的介绍,说是server端不需要保存发送到客户端的token,那如何验证客户端请求header中的token的合法性?
怎么确定客户端发过来的token不是伪造的?请指教
回复 arden 2016-03-27 23:20:08
取消用户喜欢的标签的接口,这个跟 参数是否签名有毛关系,这种问题应该是属于程序问题,没有对SQL注入做检测。
回复 梦康 2016-03-27 23:22:08
回复arden: 举例不当,谢谢指正。
回复 mervynlh 2016-04-16 15:21:35
请教下, Api Key + Security Key + Sign,这种模式下,我每一个请求都要在客户端那里生成 一个 sign吗,这样是不是客户端集成很繁琐?
回复 路人甲 2016-04-20 15:06:05
这个就是自己实现了一下非对称加密,,何不直接用HTTPS
回复 康哥 2016-04-21 22:23:18
回复路人甲: 这里不是非对称加密,是对客户端的请求一种真实性的认证,防止用户轻易伪造客户端请求(除非反编译破解加密算法)。HTTPS 也可以是被代理的。以上是我的理解,哪里不对还望指正。
回复 康哥 2016-04-21 22:24:17
回复mervynlh: 有一点点,不太麻烦,只是在构建http请求的时候封装一次即可吧。