文章主要讲了移动api调用的授权和验证问题,原文链接:The Ultimate Guide to Mobile API Security
移动API的使用是Stack Overflow和 Stormpath支持频道上经常出现的话题。这是一个已经被解决的问题,但是需要大量必要的知识和充足的理解才能很好地实现。
这篇文章能够让你了解到有关于在移动设备上安全的调用Restful API的一切内容,无论你是在构建一个需要访问Restful API的移动app,还是正在写Restful API并且打算和移动app开发者进行交互。
我的目标不仅仅是解释如何确保移动开发者能够安全调用你的Restful API,同时还解释整个凭证从开始到结束的交换过程,以及如何从安全漏洞中恢复等等。
移动API安全问题
在我们投身去探究如何确保移动开发者能够安全调用Restful API之前,让我们先讨论一下什么使得移动认证有别于传统的API认证。
API认证最基本的形式就是一般为我们所知的HTTP Basic Authentication。
对于API服务开发者和使用它的人来说,它的工作方式是非常简单的:
- 开发者会获取到一个API key(一般是ID和Secret)。这个API key一般像这样:
3bb743bbd45d4eb8ae31e16b9f83c9ba:ffb7d6369eb84580ad2e52ca3fc06c9d
。 - 开发者有责任将API key存放在一个安全的地方,没有人可以得到它。
- 开发者发起API请求时,需要把API key放到
HTTP Authorization header
中,同时带有关键词Basic
(也就是用户名和密码都使用base64加密)。下面展示了开发者在调用API时如何指定API key进行认证,使用命令行工具cURL
。
$ curl --user 3bb743bbd45d4eb8ae31e16b9f83c9ba:ffb7d6369eb84580ad2e52ca3fc06c9d https://api.example.com/v1/test
cURL
工具可以将获取到的API证书用base64加密,然后创建一个HTTP Authorization hearder
,如下所示:Basic M2JiNzQzYmJkNDVkNGViOGFlMzFlMTZiOWY4M2M5YmE6ZmZiN2Q2MzY5ZWI4NDU4MGFkMmU1MmNhM2ZjMDZjOWQ=.
。而API服务器将会从HTTP Authorization hearder
中获取字符串,然后用base64解密,获得ID和Secret,验证通过后再处理相应的API请求。
HTTP Basic Authentication虽然简单,但是很好用。开发者在调用API时附带API key可以很好地与API服务器进行验证。
但是,由于移动app无法安全存储API秘钥,使得HTTP Basic Authentication并不是一个很好的选择。同时,HTTP Basic Authentication要求在每次请求时使用原始的API keys,这就导致了keys会被长期使用,这是不安全的。
因此在大多数情况下,这种验证方式是不切实际的,因为无法安全的将API keys嵌入到分配给许多用户的移动app中。
比如说,你将API keys嵌入到你所构建的移动app中,然而一个精明的用户能够对你app进行反向工程,获取到keys,从而滥用你的API服务。
这就是为什么在不可信的环境中,HTTP Basic Authentication不是一个最好的选择,例如web浏览器和移动app。
注意:和其他所有认证协议一样,HTTP Basic Authentication必须在SSL之上使用。
为移动安全引入OAuth2.0
在此之前你可能听说过OAuth,并且在争论它是什么和它并没有足够的好。那么让我们明确:OAuth2是一个优秀的协议,在不受信任的设备中用于保护API服务的安全。它通过一种我们称之为token的认证方式来对移动用户进行认证。
下面我们从用户的角度OAuth2 token认证的工作方式。
- 用户打开移动app,输入账号和密码。
- 移动app发送一个带有用户的用户名和密码的POST请求到API服务器,服务器进行验证,成功后返回一个code。
- 移动app通过code向认证服务器发送请求,认证成功后为用户生成一个一段时间后就会过期的access token。
- 将access token存储在移动设备本地,就像一个能够访问API服务的API key一样。
- 一旦access token过期就不再工作,需要再提示用户输入用户名密码,重复1的步骤。
之所以说OAuth2在保障APIs安全上是极好的,是因为我们不需要在一个不安全的环境中长期存储API keys,取而代之的是生成一个临时的access tokens。这可以抵御一些潜在的攻击。
现在,当您的API服务生成您的移动应用程序需要的Oauth2 token时,您当然需要将其存储在您的移动应用程序中。
但是存在哪里?!
token存储的位置取决于你所处的开发平台。如果你开发的是一个android app,你会将access tokens存储在SharedPreferences中,如果你的IOS开发者,你将会将access tokens存在Keychain中。
如果你仍然存在疑问,下面这两个Stack Overflow的posts可能会非常有用,它详细阐述了如何在移动app中存储access token。
现在你应该对OAuth2有所了解,为什么要使用它,以及它的工作原理。
Access Tokens
让我们来讨论一下access tokens。他们到底是什么?是一串随机生成的数字?是uuid?还是别的?
这是一个很好的问题。
这里是一个比较短的答案:access token在技术上你任何你想要的字符串:
- 一个随机数
- 一个随机字符串
- 一个uuid
- 更多
更详细的解释:
- 为客户端而生成的。
- 验证token的所有者是你(使用强的签名)
- 存在过期时间。
有了以上的解释,你可能会希望去遵守一定的规范。为了不自己处理这些,你可以直接使用JWT(Json Web Token)。这是一个比较新的规范,允许你生成access token。这个规范(RFC7519)满足一下几点: - 可以为客户端生成
- 可以被创建者验证
- 在某一具体时间后自动过期
- 可以容纳可变的JSON信息
- 无需查询API服务,允许用户本地验证API凭证,从而减少对API调用的次数。
JWTs看起来像随机生成的字符串,在使用的时候你可以像字符串那样存储他们。这使得取代传统的access token而使用JWTs非常方便,毕竟它们基本上都是相同的,而JWT有更多的优势。
JWTs总是以加密的方式被验证。他们工作的方式如下:
- 在API服务器的某处存储一个安全随机字符串。一般是一个比较长的随机字符串(40个字符左右差不多)
- 当你创建一个新的JWT时,你需要将这个随机串传递个JWT类库来签署token,同时带有一些你想存储的数据,例如用户ID,email等等。
- token将会被生成,看起来像
header.claims.signature
——header、claims和signature都是base64加密的字符串。 - 讲生成的token给用户,一般是API的使用者,例如移动app。
现在从移动客户端中,你可以看到存储在JWT中的任何东西。so如果我有一个JWT,我可以轻松地查看到里面包含的JSON数据,通常如下所示:
{
"user_id": "e3457285-b604-4990-b902-960bcadb0693",
"scope": "can-read can-write"
}
当然,这是一个100%虚构的例子,但是你可以从中知道,如果你得到这个JWT的副本,你也可以看到存储在里面的JSON信息。
JWT标准支持自动的过期标记,因此你也能够验证这个JWT是有效的。无论你使用什么开发语言,只要在使用JWT库,你就能验证JWT的有效性直到过期!
这意味着,如果你使用JWT访问API服务,则只需验证JWT,就可以知道你的API调用是否可以工作,不需要API调用。
现在,一旦你有了一个有效的JWT,你也可以在服务器端用它做很酷的事情。
假设您已经向移动app发布了一个包含以下数据的JWT:
{
"user_id": "e3457285-b604-4990-b902-960bcadb0693",
"scope": "can-read can-write"
}
假设移动app上有恶意代码能够修改你的JWT,比如说:
{
"user_id": "e3457285-b604-4990-b902-960bcadb0693",
"scope": "can-read can-write can-delete"
}
如果这个修改过的token被发送到我们的API服务器会发生什么?它会起作用吗?我们的服务器会接受这个修改的JWT吗?
注意!!
当你的API服务收到JWT并进行验证时,它会做以下几件事情:
- 通过使用只有服务器才知道的随机生成的字符串来检查token,确保它没有被篡改。如果JWT完全被修改,这个检查就会失败,你会知道有人正在试图做一些令人讨厌的事情。
- 同时服务器会检查JWT的过期时间,确保它的合法性。so如果客户端用一个已经过期的token来进行请求,你可以立刻拒绝。
这是很好的功能,因为它使处理验证/到期/安全更简单。
在使用JWTs时,你唯一需要记住的就是:不要在JWT中存储敏感信息。
谨记以上提到的规则,那么在使用JWTs时就不会出错了。
通常你会存一下两条信息在JWTs中:
- 用户帐户的某种唯一ID。这样,当您收到此JWT进行身份验证时,您可以从用户数据库中查找此用户。
- 用户权限。当然如果构建的是所有用户都能访问的简单的API,那么这个可能是不必要的。但是通过这个,用户就不需要通过看API文档来了解自己可以做什么,不可以做什么,只需要根据看自己的JWT就可以。
以上就是有关于JWT的全部。
JWT是如何工作的
在本节中,我们将深入讨论整个流程,包括从开始到结束的整个流程,以及构建安全API服务所需的所有低级技术细节,这些API服务可以从移动设备安全地使用。
具体的工作流程如下图所示:
1. User Opens App
用户打开app,然后下一步。
2. App Asks for Credentials
由于我们准备使用OAuth2密码授权类型方案来根据我们的API服务对用户进行身份验证,因此您的应用程序需要询问用户的用户名,电子邮件地址和密码。
现在几乎所有的移动应用程序都要求这样做,所以用户习惯于输入这些信息。
3. User Enters their Credentials
用户输入自己的凭证信息,下一步。
4. App Sends POST Requests to API Service
这是最初的OAuth2流程开始的地方。你将要做的是从你的移动应用程序向你的API服务发送一个简单的HTTP POST请求。
这是一个使用cURL
来发送POST请求的实例:
$ curl --form 'grant_type=password&username=USERNAMEOREMAIL&password=PASSWORD' https://api.example.com/v1/oauth
我们在这里做的是将用户名或电子邮件和密码发送到我们的 API 服务,使用 OAuth2 密码授予类型: (有几种授予类型, 但这是一个我们将谈论这里, 因为它是唯一相关的, 当讨论建立自己的可移动的 API)。
5. API Server Authenticates the User
接下来发生的事情是,API服务将去取出传入的用户名或电子邮件和密码数据,并验证用户的凭据。
这一步不同的开发平台会有不同,但是一般步骤如下:
- 通过用户名和密码查询用户的账户信息。
- 比对加密的密码信息。
- 如果验证成功,进行下一步,否则返回错误信息,认证不合法。
6. API Server Generates a JWT that the App Stores
现在服务器已经对应用的OAuth2请求进行了身份验证,需要为该应用生成access token。为此,服务器将使用JWT库来生成有用的access token,然后将其返回给应用程序。
- 根据JWT的规则生成access token。
- 以JSON的形式返回,如下
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY",
"token_type": "bearer",
"expires_in": 3600
}
正如你上面看到的,我们的JSON响应包含3个字段。第一个字段access_token是实际的OAuth2访问令牌,移动应用程序将从这一点开始使用,以便进行已认证的API请求。
第二个字段token_type
只是告诉移动应用程序我们正在提供什么类型的访问令牌 - 在这种情况下,我们提供了一个 OAuth2 Bearer token。稍后我会再谈这个。
最后,提供的第三个字段是expires_in
字段。这基本上是提供的access token有效的秒数。
在上面的例子中, 我们要说的是, 我们给这个移动应用程序一个access token, 它可以被用来访问我们的私有 API 长达1小时-没有更多。在1小时 (3600 秒) 之后, 此access token将过期, 并且我们使用该access token所做的任何未来 API 调用都将失败.
在移动应用程序方面,你将检索此 JSON 响应, 解析 API 服务器提供的access token,然后将其存储在本地的安全位置。在 Android 上, 这意味着 SharedPreferences
, 在 iOS 上, 这意味着Keychain
。
既然已经将access token安全地存储在移动设备上,则可以将其用于向API服务器发出所有后续API请求。
7. App Makes Authenticated Requests to API Server
现在要做的就是从您的移动应用程序向您的API服务提出安全的API请求。
在最后一步中,移动应用获得了OAuth2访问令牌,然后将其存储在本地设备上。
为了使用此toekn成功发出API请求,您需要创建一个使用此token来标识您的用户的 HTTP Authorization header。
要做到这一点,你要做的就是将你的access token和Bearer
一起插入到HTTP Authorization header中。以下是使用cURL的实例:
$ curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY" https://api.example.com/v1/test
最终, Authorization header长这样:
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY
当你的API服务器收到这个HTTP请求后,会做以下工作:
- 检查 HTTP Authorization header的值,是否从
Bearer
开始。 - 获取后面字符串的值,作为token。
- 验证这个JWT是否有效,若有效,base64解密后获取用户ID、权限等信息,进行余下的API接口调用。
(完)
总结
这篇文章介绍了当下移动app API调用的认证方法。讲到了一般的HTTP认证方式、OAuth2.0认证以及JWT,对他们的原理都描述的很清楚了,所以翻译出来给大家看。
唯一觉得有一点会让读者误解的的就是对OAuth2.0中access token和JWT之间的关系的描述。所以作如下梳理:
- OAuth2.0是一种授权框架,JWT是一种认证协议。
- 在OAuth2.0中,access token其实就是一个随机串,不同开发语言和不同的安全框架生成的access token均不同。当然也可以像文中提到的使用JWT作为其access token。
- OAuth2.0通过code,去请求授权服务器时,返回信息如下。返回的数据中包含
expires_in
,代表过期时间。而JWT生成的token,为点分三段式节后,通过base64解密后,就可以获取到过期时间,而不需要单独返回一个字段。
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"example_parameter":"example_value"
}
- OAuth2.0中的access token完全是随机产生,本身无任何意义,为了进行验证需要将生成的access token和过期时间存数据库。而JWT本身可以携带一些非敏感信息和过期时间,并且本身可以通过base64解密,不需要存数据库。由于这个原因,如上文所说,就将JWT引入OAuth2.0中,作为access token,这样服务器就不需要维护Token存储,资源服务器也不必要求Token检查。