De42f693d8bd9fe1ccc6c0ca49fa1ebe
013 | 如何设计安全的Web API

关于如何设计一套优秀的 Web API,其学问也挺大的,要考虑的问题其实并不少,从大的来说,比如:安全和性能之间如何平衡?采用 REST 风格还是 RPC 风格抑或混合风格?如何设计接口的粒度?从小的来说,比如:某个接口是要返回整个对象的所有数据还是只返回客户端目前需要的数据?HTTP 状态码和业务错误码是分开定义还是统一定义?等等……另外,不同人会有不同的设计偏好,所以也很难说哪种设计更优秀。其实,无需纠结太多,谨记一个原则即可:适合的才是最好的。当然,一篇文章讲不了这么多内容,本篇文章主要从安全方面来聊聊如何设计 Web API,其他方面有机会我会在其他相关文章穿插聊一聊。

安全 vs 性能

安全和性能就是一对冤家,想要满足高安全性,就得牺牲掉一些性能,而想要达到高性能,就只能减低些安全性,这是无可避免的,你很难做到让这两方都非常满意,你只能平衡取舍。

就举个简单的例子来说明安全和性能是如何平衡的,通过之前的文章我们已经了解到,公钥加密比对称加密安全性更高,但加密和解密的性能比对称加密却慢了几个数量级,那么,在实际应用中,为了平衡安全和性能,长消息还是使用对称加密,而对称加密的密钥才使用公钥加密。当然,这种方案安全性比不上只使用公钥加密,性能上也比不上只使用对称加密,但却能够很好地保证了一定的安全和性能,这就是一种平衡取舍。同理,对一串消息进行数字签名时,也不是对整个消息直接用私钥进行签名,而是对消息先计算出消息摘要,再对消息摘要进行数字签名。

很多时候,我们要做到安全和性能的平衡,是需要结合多种技术的,这就要求你的技术知识面要广一些,以及要有整合能力,才知道要结合怎样的技术才能达到最优的平衡效果。

另外,从产品的角度来说,不同的产品,以及同个产品的不同发展阶段,对安全和性能的要求也都不同。资讯类产品对安全的要求一般就比较低,社交类产品对安全要求会高一点,电商类产品因为涉及到金额,安全性要求自然较高,金融产品则涉及到大量金额,因此对安全性要求无疑是最高的。从是否对外开放来说,开放性 API 一般也比封闭性 API 的安全性要求高一些,当然,你不能说是一个开放性的资讯类 API 和封闭性的金融类 API 对比,而应该是同类产品来比较。

简而言之,当我们设计一个东西时,不能片面地看问题,要根据具体的产品、具体的发展阶段,综合考量各种潜在性问题,最后得出适合当下的最优方案。设计 Web API 也一样。

HTTPS 是首选

Web API 方面,如果安全设计不当,是会存在很多安全隐患的,比如:敏感信息泄露、数据遭篡改、权限控制漏洞、SQL 注入、HTTP 劫持、XSS 攻击等等。应对不同的安全隐患,可以有不同的安全方案,而首选的安全方案应是 HTTPS。并不是说有了 HTTPS 就能防范所有安全隐患,但正确使用 HTTPS 确实能防范大部分安全隐患,包括但不限于敏感信息泄露、数据遭篡改、HTTP 劫持、中间人攻击等。

另外,现在大部分项目也都实现了全站 HTTPS,HTTPS 已经成为一种必然的趋势。苹果之前也要求所有上架 AppStore 的 iOS App 需在 2017 年强制使用 HTTPS,虽然后来启用了 HTTPS 的 App 比例依然很低从而苹果只能推迟了这项强制性要求,虽然苹果现在还没有公布新的 deadline,但也只是早晚的事,所以,HTTPS 以后应该会成为标配。

使用 HTTPS 最关键的安全因素在于数字证书,尽量用权威的 CA 证书,而不是用自签名证书,而且最好加上证书绑定的强校验,这在前一篇文章已经讲过,就不再赘述了。

另外,也要防范攻击者进行降级攻击,这主要是利用了 SSL v3 版本的 POODLE 漏洞,通过该漏洞,攻击者可以获取到 SSL 通信中的部分信息明文。为了进行降级攻击,在握手阶段,与服务端协商使用的协议版本时,攻击者会与服务端协商使用 SSL v3,而不是采用更高版本的加密协议。低版本的加密协议除了存在这个漏洞,也存在其他安全漏洞,因此,建议在 Nginx 配置只支持更安全的高版本 TLS 协议:

  • ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

为了进一步减少安全漏洞,建议也要启用 HSTS 策略,以及 80 端口的 HTTP 请求都重定向转为 443 的 HTTPS 请求:

  • server {
  • listen 80;
  • server_name example.com;
  • add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;preload" always;
  • return 301 https://$server_name$request_uri;
  • }

用户鉴权

用户鉴权,就是当用户发起某个请求时,鉴别该用户是否有权限访问该请求对应的数据资源。用户鉴权基本是大部分 API 都需要有的功能,除了那些没有用户体系的 API。用户鉴权可以分为两部分:一是用户登录时的鉴权,二是登录之后的鉴权。

先来聊聊登录时的鉴权,大部分应用都是用户名 + 密码的登录方式,也有小部分应用采用手机号 + 短信验证码的登录方式,而像火币网、币安这些数字货币交易平台除了用户名 + 密码,还加上了谷歌验证码的校验,另外,像广州公积金管理中心还提供了刷脸登录的方式。不同的登录方式,主要是对于安全性的要求不一样。另外,提高了安全性,一般也意味着牺牲了一些用户体验,因此,具体应用需要根据具体情况去设计合适的用户鉴权方案,不管是登录时的鉴权,还是登录后的鉴权。

大部分应用都是采用用户名 + 密码的登录方式,因此,我们重点来聊聊这个。这个方案的核心在于要保证密码的安全。不少应用直接对密码进行 MD5 之后传输给服务端,他们觉得这样子已经足够安全了,因为 MD5 是不可逆的,所以破解不了源密码。但实际上,对于服务端来说,真正的密码就是这串 MD5 之后的字符串,攻击者也不需要知道它的源密码。所以,不管你是做一次 MD5,还是多次 MD5,或改用 SHA1/SHA256,结果都一样,攻击者只要拦截到你的请求,拿到你这串加密后的密码,他就可以伪装真实用户通过服务端的鉴权。另外,如果用户的密码比较简单,攻击者还可以通过彩虹表破译出源密码。为了应对彩虹表攻击,所以很多平台要求密码要足够复杂,比如密码要包括大小写字母和数字。还有,如果不同用户设置了相同密码,那 MD5 后的结果也一样,那攻击者破译了其中一个用户的源密码就等于破译了多个用户的源密码。

top Created with Sketch.