前后端分离是个浪潮,原来只有APP客户端会考虑这些,现在连Web都要考虑前后端分离 。

这里面不得不谈的就是API的设计和安全性,这些个问题不解决好,将会给服务器安全和性能带来很大威胁 。

API的设计中,主要考虑两大方面的问题 :

  • 防止API被恶意调用

  • API通信中数据加密的问题

由于HTTP协议是无状态的,所以在做MVC Web的时候,无论是Java Web还是PHP等,大多数都是依靠session/cookie来完成的用户标识的。但在前后端分离的开发模式中,session/cookie模式就显得不太合适,尤其是APP客户端,是不太可能用session/cookie的,业界广泛采用的方式就是采用token。

我们用楼、楼管和租户来做个类比。我们的系统是一栋楼,一名管理员负责管理这栋楼和租户,有100个租户,每个租户都被分配了属于自己的一个房间而且租户不可以随意进入其他租户的房间。当租户第一次来登记的时候,楼管就会要求租户出具身份证(用户名和密码),在核验完毕身份证后(验证密码)后,管理员会把一件房的钥匙(token)给租户并同时自己也记录下钥匙和房间号。自此之后,用户每次进出自己房间只需要用自己的钥匙即可。但后来楼管觉得用户长期持有同一把钥匙有些不安全,比如钥匙被别人克隆了一把又或者钥匙丢了让别人捡了,这样会比较危险,所以楼管又决定给每把分配出去的钥匙有效时间,比如30天。当钥匙到期后,就不可以再打开门锁,必须只能再找楼管换一把新钥匙。

回到正文中,我们引入两个API来说明具体开发中流程。现在有两个API:

  • http://host.com/api/account/login 用户登录API

  • http://host.com/api/order/list 用户订单列表

TIPS :

下面流程仅仅是大体流程,不处理具体细节。方案都是为了说明问题才用简单粗暴,可以根据大概原理自己加入更丰富的元素
伪代码中出现的post不表示使用POST方式提交数据,仅仅表示人们口中常说的PO数据
本文仅为API设计提出大体方案,比如签名机制,但具体签名机制采取多少数据项、用md5或者sha1可自主决定

客户端需要根据服务端API文档首先实现登录页面,假如文档要求以POST方式json协议提交数据,伪代码演示如下:

http.post( 'http://host.com/api/account/login', { "account":"zhangsan", "password":"" } )

服务端收到数据后,从数据库中验证用户名和密码(检查租户身份证),如果错误,返回错误提示,如果正确,就要生成一个token(颁发钥匙)给用户并同时自己也要记录下token是代表了哪位用户(记录下钥匙给了哪个用户)。假如用户的uid是8,生成的token是abcdefg,那么也就是说abcdefg这个token分配给了8号用户(8号租户持有钥匙abcdefg),客户端自己需要保存住这个token(租户自己持有钥匙)。

当用户需要访问自己订单的时候,也就是需要访问API http://host.com/api/order/list 的时候,就要带上token,因为服务端是记录了token和用户uid对应关系的,所以服务器就可以根据token得知当前访问的用户是谁并返回给该用户其订单内容,伪代码演示如下:

http.post( 'http://host.com/api/order/list', { "token" : "abcdefg" } );

这样就基本上已经实现了客户端和服务端通信了,但实际上仅仅这样还是有很大的安全风险。如果任何一个人通过抓包等方式得知了服务器API的地址,就意味着他可以任意调用API了,我们的API会被盗用。为了避免这种被盗用,引入签名机制。也就说访问任何一个API的时候,都需要验证签名,只有签名通过了才可以继续下去,否则就会弹出错误信息。伪代码演示如下:

// 这里可以看到签名的机制就是将api使用md5 hash一下
// 访问帐号登录API
http.post( 'http://host.com/api/account/login', { "account":"zhangsan", "password":"" } ).signature( 'api/account/login' )
// 访问我的订单API
http.post( 'http://host.com/api/order/list', { "token" : "abcdefg" } ).signature( 'api/order/list' )

服务端收到数据后,也用相同的签名方式运算出签名与客户端传递来的签名进行对比,伪代码演示如下:

server_signature = md5( 'api/account/login' )
client_signature = http.get_post( 'signature' )
if ( server_signature != client_signature ) {
return 'signature error';
}

在具备这种签名机制后,如果客户端被反编译了,签名机制就会被人得知。所以,在签名机制中引入另外一个新的重要元素:时间戳。时间戳的引入有两个重要作用:

  • 判定某次API访问的时效性

  • 参与签名运算

假如某次访问 http://host.com/api/order/list 的时候timestamp值为123456789,签名为”xyz”,有恶意用户记录下该所有的数据然后反复调用。如果我们在服务端对比服务器时间和用户提交过来的时间戳,两者相差巨大超出一天或者半个小时,那么就可以直接返回一些诸如“过期的API访问”等等错误提示。

事情做到这里看起来已经基本比较完善了,这样的签名制度看起来能够抵挡相当大一部分恶意调用了。实际上,在真正完善的API设计中,API都会由API网关来实现,API网关中有一项功能就是防刷限流,可以根据不同维度比如用户、IP地址、设备ID来限制其每秒钟内对某个API的最多访问次数。

截止到目前为止,对于敏感数据包括token在内,我们都是明文传输的。我们需要对敏感数据加密,假如此时产品经理提出了第三个要求:添加银行卡,银行卡号算是敏感信息吧。

至于数据保密性的问题,我们第一点想起的自然是https了。但是,https在面对charles等抓包工具时,其实并没有什么卵用,只要配置一下根证书瞬间可以看到一切明文,所以,除了必要的https外,我们还需要额外的加密机制。

假如这个API是http://host.com/api/bankcard/create ,那么加密的要求就当用户添加银行卡时如果数据被拦截后至少不能赤裸裸地将银行卡号暴露出去。我们需要引入一套加密方案,对敏感数据实现加密。

加密方案不在本文讨论范围,所以我就直接选择AES高级加密方式,AES对内容进行需要一个加密密码,伪代码演示一下:

// 加密密码
password = ''
// 需要加密的内容
message = 'Hello World!'
// 利用加密密码对内容进行加密
enc_message = encrypt( password, message )
// 解密
dec_message = decrypt ( password, enc_message )
print dec_message // Hello World!

下面我们将引入加密机制后业务逻辑流程完整走一遍

// 第一步,客户端执行登录
http.post( 'http://host.com/api/account/login', { "account":"zhengsan", "password":"" } ).signature( "api/account/login"+"timestamp" )
// 第二步,服务端收到登录需要,再对比完签名和timestamp时间有效性后,执行登录业务逻辑server_timestamp = get_timestamp()
client_timestamp = get_post( 'timestamp' )
if( server_timestamp - cilent_timestamp > ){
return '过期API访问'
}
server_signature = signature( 'api/account/login' + client_timestamp )
client_signature = get_post( 'signature' )
if( server_signature != client_signature ){
return '签名错误'
}
// 验证密码并返回token
password = get_post( 'password' )
account = get_post( 'account' )
server_password = get_password_by_account( 'account' )
if( password == server_password ){
// 生成一个AES加密密码
enc_password = "1a2b3c4d5f6g7h8i9j0k"
// 生成原始的token
token = "0k9j8h7i6g5f4c3b2c1az9y8x7"
// 服务端记录token与uid对应关系
set( token, uid )
// 最后一步很重要,要将aes加密密码 和 token返回给客户端
return enc_password,token
}
// 第三步,客户端收到登录后的数据:加解密密码 和 token,然后保存起来
token = get( 'token' )
enc_password = get('enc_password')
// 将这两项保存起来
save( token, enc_password )
// 先对银行卡号进行加密,然后再进行提交
bank_card = ''
enc_bank_card = encrypt( enc_password, bank_card )
http.post( "http://host.com/api/bankcard/create", { enc_bank_card, enc_password, token } ).signature( 'api/bankcard/create'+timestamp )
// 第四步,服务端收到数据后,验证API签名和timestamp时效性,最后解密数据,入库
// 验证signature和timestamp时效性伪代码略过...
// 获取客户端传来的token enc_bankcard enc_password
token = get_post( 'token' )
enc_bankcard = get( 'enc_bankcard' )
enc_password = get( 'enc_password' )
bankcard = decrypt( enc_password, enc_bankcard )
// 根据对应关系,用token找到uid
uid = get_uid_by_token( 'token' )
// 将uid和bankcard入库
save( uid, bankcard )
总结:
  • 签名机制是为了防止API被恶意调用,包括API

  • 加密是为了保证敏感数据,敏感数据可以包括token

  • token本身与加密无关,只是token本身的含义总是跟加密似乎带点儿关系,但实际上token仅仅是个用户身份识别器

  • 只要客户端被反编译了,加密方式和签名机制都会暴露出来,所以安全是需要双方配合的

FAQ:
    • token和uid对应关系如何实现?
      通过redis处理,你可以考虑一个hash类型数据结构,key就用token,hash中保存完整的用户信息

    • token或者aes加解密密码如何传递?
      最好不用GET方式,建议走POST方式,也可以将这些信息放到http header中,也可以放到http body中。我自己一般习惯将signature、token、timestamp、enc_password这些信息放在http header中,API参数放在http body中

    • 我怎么感觉enc_password这样明文传好危险
      其实,你可以不用完整的enc_password,你可以和客户端协商制定一个规则,比如去掉enc_password的前三位和后两位,用剩下的做加解密密码

    • token本身需要加密吗?有没有必要所有api提交的参数都加密?
      可以加密,你甚至用解密后的token参与签名运算,制作出更复杂的签名规则。至于提交参数是否都加密,实际上是可以的。如果任何api参数都加密,抓包者是无法通过抓包分析你api接受参数的名称的,比如原来是明文提交{“account”:”zhangsan”},如果该api加密提交,那么这个json被加密成”abcdekkadadfad==”之流,抓包者由于无法得知确切的参数名称account就无法很容易写出一些脚本

API的设计与安全的更多相关文章

  1. RESTful API URI 设计的一些总结

    非常赞的四篇文章: Resource Naming Best Practices for Designing a Pragmatic RESTful API 撰写合格的 REST API JSON 风 ...

  2. RESTful API URI 设计: 查询(Query)和标识(Identify)

    相关文章:RESTful API URI 设计的一些总结. 问题场景:删除一个资源(Resources),URI 该如何设计? 应用示例:删除名称为 iPhone 6 的产品. 是不是感觉很简单呢?根 ...

  3. RESTful API URI 设计: 判断资源是否存在?

    相关的一篇文章:RESTful API URI 设计的一些总结. 问题场景:判断一个资源(Resources)是否存在,URI 该如何设计? 应用示例:判断 id 为 1 用户下,名称为 window ...

  4. Atitit.会员卡(包括银行卡)api的设计

    Atitit.会员卡(包括银行卡)api的设计 1. 银行卡的本质是一种商业机构会员卡1 2. 会员卡号结构组成1 2.1. ●前六位是:发行者标识代码 Issuer Identification N ...

  5. Web API接口设计经验总结

    在Web API接口的开发过程中,我们可能会碰到各种各样的问题,我在前面两篇随笔<Web API应用架构在Winform混合框架中的应用(1)>.<Web API应用架构在Winfo ...

  6. 优秀的API接口设计原则及方法(转)

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  7. atitit.基于http json api 接口设计 最佳实践 总结o7

    atitit.基于http  json  api 接口设计 最佳实践 总结o7 1. 需求:::服务器and android 端接口通讯 2 2. 接口开发的要点 2 2.1. 普通参数 meth,p ...

  8. paip.复制文件 文件操作 api的设计uapi java python php 最佳实践

    paip.复制文件 文件操作 api的设计uapi java python php 最佳实践 =====uapi   copy() =====java的无,要自己写... ====php   copy ...

  9. 好RESTful API的设计原则

    说在前面,这篇文章是无意中发现的,因为感觉写的很好,所以翻译了一下.由于英文水平有限,难免有出错的地方,请看官理解一下.翻译和校正文章花了我大约2周的业余时间,如有人愿意转载请注明出处,谢谢^_^ P ...

  10. 关于API的设计与实现

    http://blog.csdn.net/horkychen/article/details/46612899 API的设计是软件开发中一个独特的领域.最主要的特殊点在于API是供开发者使用的界面,即 ...

随机推荐

  1. 【2018.05.10 智能驾驶/汽车电子】AutoSar Database-ARXML及Vector Database-DBC的对比

    最近使用python-canmatrix对can通信矩阵进行编辑转换时,发现arxml可以很容易转换为dbc,而dbc转arxml却需要费一番周折,需要额外处理添加一些信息. 注意:这里存疑,还是需要 ...

  2. February 16th, 2018 Week 7th Friday

    Full of luck, health and cheer. We wish you a Happy Chinese New Year! 春节快乐,万事如意! From Shanbay. Today ...

  3. 类中的 this关键字

    this可用于区分局部变量和成员变量,因为构造函数中如果使用 this.成员变量 = 参数值, 那么可以在new对象时,将初始化值赋值给成员变量,否则成员变量赋值失败, 所以this可以区分成员变量和 ...

  4. css滚动条样式自定义

    很简单的几行代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...

  5. 转://Oracle 事务探索与实例(二)

    一数据库版本 SYS@LEO1>select * from v$version; BANNER ------------------------------------------------- ...

  6. Linux之RTOS学习

    Linux之RTOS学习 RTOS: Real time operating system 系统选型 可选方案 RTLinux - FSMLabs, WindRiver Systems - http: ...

  7. automake - 使用 autotools 工具集

    一般而言,对于小项目或玩具程序,手动编写 Makefile 即可.但对于大型项目,手动编写维护 Makefile 成为一件费时费力的无聊工作. 本文介绍 autotools 工具集自动生成符合 Lin ...

  8. python3 练习题 day04

    '''1.整理装饰器的形成过程,背诵装饰器的固定格式''''''开放封闭原则:为了保证程序的稳定性,和功能的可开放性,在不修改目标函数源代码和调用方式的情况下,对目标函数增加新功能'''# def w ...

  9. Ubuntu16.04安装和使用ElasticSearch

    1.下载es wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/ela ...

  10. Python脱产8期 Day12 2019/4/26

    一 函数默认值的细节 1.如果函数的默认参数的默认值为变量,在所属函数定义阶段一执行就被确定为当时变量存放的值 例: a = 100def fn(num=a): print(num) # 100a = ...