Token Vending Machine:移动应用客户端安全访问 AWS 服务的解决方案

阅读数:121 2019 年 11 月 15 日 08:00

Token Vending Machine:移动应用客户端安全访问AWS服务的解决方案
### 背景介绍
复制代码
广大移动互联网应用和移动游戏开发者在利用 AWS 服务进行开发过程中,经常需要为移动客户端提供 AWS 服务访问安全证书,以便让这类移动端应用有权限直接访问 AWS 服务,比如通过 AWS S3 服务上传图片文件或者通过 AWS SQS 服务发送消息。
有些移动开发者可能会考虑为每个移动应用用户分配一个固定的 AWS IAM 安全证书来实现移动客户端访问 AWS 服务。但是一款热门的移动互联网应用或者移动游戏往往拥有数百万甚至上千万的用户基数,让系统管理员为每一个用户分配和管理 IAM 安全证书工作量将会非常巨大。而且移动客户端相对服务器端具有较低的安全等级,保存在移动设备内部的敏感信息比如用户账号或密码存在泄露的风险,强烈建议移动开发者不要将 AWS 安全证书长期保存在用户的移动设备中。
利用 AWS 安全令牌服务 Security Token Service (简称 STS)可以动态的为大量移动客户端用户分配临时安全证书 ,并且可以限制这些临时安全证书的 AWS 服务访问权限和有效时间。使用 AWS STS 临时安全证书没有用户总数的限制,也不需要主动轮换,证书自动会过期,拥有非常高的安全性。对于这种采用 AWS STS 和其他相关 AWS 服务构建的移动客户端访问 AWS 服务安全证书分配系统,我们把它命名为 Token Vending Machine,即令牌售卖机,简称 TVM。
下面我们以一个典型的手机图片管理 APP 演示项目为例来介绍如何利用 AWS 相关服务设计和开发一套 TVM 系统。读者可以通过参考演示项目的设计思想和相关源码快速开发出符合自己项目需求的 TVM 系统。
假设该演示项目的基本需求如下:
1) 用户在使用该 APP 前要先完成注册
2) 用户成功登录后可以通过 APP 上传,查看和管理自己的图片
3) 用户不可以访问到其他用户的图片
### 实现原理
!
整个演示项目实现可以分为三个主要模块:移动客户端、TVM 系统和 S3 服务。
**A. 移动客户端 **
* 包括访问 TVM 系统获取临时安全证书的客户端代码
* 包括直接访问 AWS S3 存储桶用户个人目录内容和图片管理相关的代码逻辑实现。
**B. TVM 系统 **
* 使用了一台 AWS EC2 实例来运行 Apache Tomcat Web 服务器,用于向移动客户端提供远程访问接口以获取临时安全证书。在 Tomcat 内部则部署了使用 JAVA 语言开发的 TVM 服务器端实现。
* 使用了 AWS 高性能的 NoSQL 数据库 DynamoDB 做为后台用户数据库。该数据库用来保存注册用户的账号、密码和会话 Key 等信息。
开发者自行设计和实现 TVM 系统的时候,完全可以使用自己熟悉的数据库产品或者集成第三方已有的用户数据库服务,比如基于 LDAP 的企业内部用户数据库。
* TVM 系统的 JAVA 实现通过访问 AWS STS 服务获取临时安全证书以提供给移动客户端。
* 在真实的项目中,运行 TVM 系统的服务器端往往还将直接管理 S3 中保存的所有用户资源,比如可以限制每个用户允许上传图片的数量和文件合计大小等等。这部分功能在本演示项目中暂时没有实现。
**C. S3 服务 **
* AWS S3 服务为用户上传图片提供了持久化存储能力。
在用户成功完成账号注册后,TVM 系统的基本工作流程如下:
1) 用户通过移动客户端输入账号和密码,登录系统。
2) TVM 查询用户数据库,校验账号和密码组合的合法性。
3) TVM 访问 AWS STS 服务,请求分配临时证书,TVM 将获得的临时安全证书返回移动客户端。
4) 移动客户端使用获取的临时安全证书,调用 AWS S3 API,执行文件的上传、列表和下载等操作。
### 部署过程
1. 使用 IAM 用户账号登录 AWS 控制台
2. 创建 IAM EC2 角色
3. 创建临时安全证书角色
4. 在 Launch TVM EC2 实例的过程中,选择使用创建的 IAM EC2 角色
5. 在 TVM EC2 实例中部署 Tomcat 和 TVM war 包
6. 下载并安装 TVM apk 文件到安卓移动终端
### 细节说明
**IAM EC2 角色定义 **
基于对生产环境高安全性要求的考虑,我们没有在 JAVA 代码中直接使用静态配置的 IAM 用户 Access Key Id 和 Access Key 来访问 AWS DynamoDB, S3 和 STS 等 AWS 服务,而是希望使用 AWS 动态分配的临时安全证书。为此我们创建了一个专门的 IAM EC2 角色,并为该角色赋予了足够的 AWS 服务访问权限。这样一来,运行在带有该 IAM 角色的 EC2 实例中的 TVM 组件,就可以通过 EC2 上下文获得拥有足够 AWS 服务访问权限的临时安全证书。请注意不要将 TVM 组件自己使用的临时安全证书与 TVM 组件将为移动客户端分配的临时安全证书相混淆。这里通过 EC2 上下文获取的临时安全证书主要用于 TVM 组件在服务器端访问 AWS 相关服务,比如读写 DynamoDB 或者向 STS 服务请求为移动客户端分配临时安全证书。
下面的例子 IAM Policy 文件赋予了 IAM EC2 角色访问 AWS STS 服务的 AssumeRole 接口和其他 AWS 服务的权限。开发者可以根据自己的实际需求增加或减少相关权限分配。
`{`
` "Version": "2012-10-17",`
` "Statement": [`
` {`
` "Effect": "Allow",`
` "Action": "sts:AssumeRole",`
` "Resource": "*"`
` },`
` {`
` "Effect": "Allow",`
` "Action": [`
` "sqs:*" ,`
` "sns:*" ,`
` "dynamodb:*"`
` ],`
` "Resource": "*"`
` },`
` {`
` "Effect": "Allow",`
` "Action": [`
` "s3:*"`
` ],`
` "Resource": "*"`
` }`
` ]`
`}`
TVM 组件实现代码在构造 STS 服务访问客户端对象的时候,我们使用了 AWS JAVA SDK 提供的 com.amazonaws.auth.InstanceProfileCredentialsProvider 证书加载类文件。该类实例可以自动访问 EC2 运行环境上下文,获取临时安全证书以供构造的 STS 服务访问客户端对象使用。并且当获取的临时安全证书即将失效时,该类实例还可以自动去获取新的安全证书。通过使用该类实例,TVM 组件开发者就不再需要考虑访问 STS 或 DynamoDB 服务时需要提供的安全证书问题。
下面的代码片段演示了如何构建一个带有自动安全证书管理能力的 STS 服务访问客户端对象。
代码片段来自于 TVM 组件的 com.amazonaws.tvm.TemporaryCredentialManagement.java 源文件。
`AWSSecurityTokenServiceClient sts =`
` new AWSSecurityTokenServiceClient(`
` new InstanceProfileCredentialsProvider() ); `
**STS API 方法选择和使用 **
AWS STS 服务提供了多个 API 方法,分别用于不同场景下的临时证书获取。其中的 AssumeRole 方法是唯一支持临时安全证书调用的。这种 STS API 方法的调用方式看上去非常有趣:我们使用了来自 EC2 上下文的临时安全证书去调用 STS AssumeRole 方法,目的是为了帮助移动客户端用户申请访问 AWS S3 服务的临时安全证书。实际上通过 EC2 上下文获取的临时安全证书也是来自 AWS STS 服务的动态分配。这一点恰恰也证明了 AWS 服务的松耦合设计思想,用户可以通过灵活组合不同的服务来达到自己的设计目的。
!
STS AssumeRole 方法提供了多个参数,可以灵活的设置分配的临时安全证书的各种特性。我们这里主要介绍演示项目用到的几个重要参数。
<caption></caption>
col 1 | col 2 | col 3 | col 4
--------------- | ------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
** 名称 ** | ** 类型 ** | ** 必填 ** | ** 含义 **
DurationSeconds | 整型 | 否 | 以秒为单位的临时安全证书有效时间限制。最小可以是 15 分钟(900 秒),最大可以是 1 个小时(3600 秒)
默认值:3600 秒。
RoleArn | 字符串 | 是 | 临时安全证书对应的角色 Arn 值。开发者在为移动客户端分配临时安全证书的时候,需要首先在 AWS 系统中创建该角色对象,并且为角色设置适当的权限。STS AssumeRole 方法返回的临时安全证书的权限就将以该角色所拥有的权限为基础。如果开发者调用 STS API 时候还提供了 Policy 参数,返回的安全证书权限还将在此基础上做进一步限制。以两个参数提供权限的交集作为返回的临时安全证书的最终权限设置。
RoleArn 格式举例:
“arn:aws-cn:iam::358620XXXXXX:role/TVMClientRole”
Policy | 字符串 | 否 | 以 Json 格式表示的附加权限设置。如果该参数被设置,STS 服务将使用 RoleArn 参数中指定的角色对应的权限和该参数设置权限的交集来定义即将返回的安全证书的权限。一种常用的做法就是使用该参数来进一步限制返回安全证书的权限到每个具体的实体。在我们的演示项目中,就是通过设置 Policy 来进一步限制每个登录用户只能访问属于自己的 S3 文件。
RoleSessionName | 字符串 | 是 | 角色会话名称,主要用来区分申请临时安全证书的不同用户或者不同使用场景。
在我们的演示项目中,设置的角色会话名称就是用户通过手机客户端应用输入的登录名。
下面的代码片段演示了如何调用 STS AssumeRole 方法申请新的临时安全证书。
代码片段来自于 TVM 组件的 com.amazonaws.tvm.TemporaryCredentialManagement.java 源文件。
`// 构造请求对象`
`AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest();`
`assumeRoleRequest.setRoleArn("Arn of your TVM role");`
`assumeRoleRequest.setPolicy(`
` TemporaryCredentialManagement.getPolicyObject( myUserName ));`
`assumeRoleRequest.setRoleSessionName(myUserName);`
`assumeRoleRequest.setDurationSeconds(`
`new Integer( Configuration.SESSION_DURATION )); `
`// 获取临时安全证书`
`AssumeRoleResult assumeRoleResult = sts.assumeRole(assumeRoleRequest);`
`if (assumeRoleResult != null && assumeRoleResult.getCredentials() != null)`
`{ `
` log.info(" 利用 EC2 角色从 STS 服务获取临时证书操作成功!");`
` log.info("AccessKeyId = "`
` + assumeRoleResult.getCredentials().getAccessKeyId());`
`}`
`else`
`{`
` log.warning(" 利用 EC2 角色从 STS 服务获取临时证书操作失败!");`
`} `
** 设置安全证书权限 **
在我们演示项目的需求列表中,有一个需求是不同的用户只能访问 S3 对象存储服务中属于自己的文件。实现该需求有不同的方法,我们这里采用方法的是限制移动客户端使用的 AWS 临时安全证书的 S3 访问权限。在 AWS STS AssumeRole 方法中有两个参数可以设置返回的临时安全证书的权限:一个是临时安全证书角色 Arn 值,一个是附加的 Policy 字符串。
在我们演示项目的实现过程中,我们为创建的临时安全证书角色分配了如下权限策略,保证 AWS STS 服务返回的临时安全证书拥有指定 S3 存储桶的必要操作权限。
`{`
` "Version": "2012-10-17" ,`
` "Statement": [`
` {`
` "Effect": "Allow",`
` "Action": "s3:ListBucket",`
` "Resource": "arn:aws-cn:s3:::tvm-examplebucket"`
` },`
` {`
` "Effect": "Allow",`
` "Action": [`
` "s3:GetObject",`
` "s3:PutObject",`
` "s3:DeleteObject"`
` ],`
` "Resource": "arn:aws-cn:s3:::tvm-examplebucket/*"`
` }`
` ]`
`}`
请注意,在创建临时安全证书角色的过程中,** 还需要添加该角色对于之前创建的 IAM EC2 角色的信任关系 **。否则 TVM 服务器端组件在执行 AssumeRole 方法时候,AWS 系统会提示当前用户没有对临时安全证书角色执行 AssumeRole 操作的权限。
!
接下来我们将利用模板文件动态地构造附加的 Policy,目的是限制每个登录用户只能够访问自己目录下的 S3 资源。
模板文件的格式如下:
`{`
` "Version": "2012-10-17",`
` "Statement": [`
` {`
` "Effect": "Allow",`
` "Action": "s3:ListBucket",`
` "Resource": "arn:aws-cn:s3:::tvm-examplebucket"`
` },`
` {`
` "Effect": "Allow",`
` "Action": [`
` "s3:GetObject",`
` "s3:PutObject",`
` "s3:DeleteObject"`
` ],`
` "Resource": "arn:aws-cn:s3:::tvm-examplebucket/__USERNAME__/*"`
` }`
` ]`
`}`
以下的例子代码利用登录用户名替换模板中的“__USERNAME__”,构造出指定用户的权限 Policy。
代码片段来自于 TVM 组件的 com.amazonaws.tvm.TemporaryCredentialManagement.java 源文件。
`protected static String getPolicyObject( String username ) throws Exception`
`{`
` // Ensure the username is valid to prevent injection attacks.`
` if ( !Utilities.isValidUsername( username ) )`
` {`
` throw new Exception( "Invalid Username" );`
` }`
` else`
` {`
` return Utilities.getRawPolicyFile()`
` .replaceAll( "__USERNAME__", username );`
` }`
`}`
** 权限分级控制 **
在本演示系统中,用于开发和部署 TVM 系统的 IAM 用户、最终运行 TVM 系统的 EC2 实例对应的 IAM 角色和移动客户端所获得的临时安全证书分别拥有不同大小的权限,实现了很好的权限分级控制。
!
** 移动客户端临时安全证书的过期问题处理 **
在前面我们介绍的 TVM 系统的基本流程里面,移动客户端应用在登录成功后,TVM 组件将直接返回临时安全证书。而实际的实现过程要比这复杂一些,主要是为了解决移动客户端获取的临时安全证书过期后的自动更新问题。
!
TVM 系统的完整工作流程如下:
1) 用户通过移动客户端输入账号和密码,登录系统。
2) TVM 查询用户数据库,校验账号和密码组合的合法性,创建并返回代表当前用户会话的 Key 值给移动客户端。
3) 移动客户端在本地缓存获取的会话 Key。移动客户端利用本地保存的会话 Key 和用户动态 ID 向 TVM 系统发起请求,申请临时安全证书。
4) TVM 系统校验移动客户端用户身份和会话 Key,访问 AWS STS 服务,请求分配临时安全证书,TVM 将获取的临时安全证书返回移动客户端。
5) 移动客户端在本地缓存获取的临时安全证书。移动客户端使用本地保存的临时安全证书,持续调用 AWS S3 API,执行文件的上传、列表和下载等操作。
关于移动客户端获取临时安全证书,请注意下面的细节:
* 在临时安全证书有效时间范围内,移动客户端可以直接使用本地保存的临时安全证书访问 AWS 服务,比如 S3 存储桶。
* 一旦临时安全证书过期,移动客户端需要凭借本地保存的用户会话 Key 和动态用户 ID 向 TVM 系统再次申请临时安全证书,不需要再提供用户名和密码信息。
* 如果是刚刚启动移动客户端或者 TVM 用户会话 Key 已经失效,移动客户端需要执行上述完整的登录和临时安全证书获取过程。
下面的代码片段演示如何登录 TVM 系统,获取当前用户的会话 Key。
代码片段来自于安卓移动客户端组件的 com.amazonaws.tvmclient.AmazonTVMClient.java 源文件。
`public Response login( String username, String password ) {`
` Response response = Response.SUCCESSFUL;`
` if ( AmazonSharedPreferencesWrapper.getUidForDevice( this.sharedPreferences ) == null ) {`
` String uid = AmazonTVMClient.generateRandomString();`
` LoginRequest loginRequest = new LoginRequest(this.endpoint,`
` this.useSSL,`
` this.appName,`
` uid,`
` username,`
` password );`
` ResponseHandler handler = new LoginResponseHandler( loginRequest.getDecryptionKey() );`
` response = this.processRequest( loginRequest, handler );`
` if ( response.requestWasSuccessful() ) {`
` AmazonSharedPreferencesWrapper.registerDeviceId(this.sharedPreferences,`
` uid, `
` ((LoginResponse)response).getKey());`
` AmazonSharedPreferencesWrapper.storeUsername( this.sharedPreferences, username ); `
` } `
` }`
` return response;`
`}`
下面的代码片段演示如何使用当前用户的会话 Key 和动态用户 ID 访问 TVM 系统,更新本地保存的临时安全证书。
代码片段来自于安卓移动客户端组件的 com.amazonaws.demo.personalfilestore.AmazonClientManager.java 和 com.amazonaws.tvmclient.AmazonTVMClient.java 源文件。
`public Response validateCredentials() {`
` Response ableToGetToken = Response.SUCCESSFUL;`
` if (AmazonSharedPreferencesWrapper.areCredentialsExpired( this.sharedPreferences ) ) {`
` // 清空本地保存的过期临时安全证书 `
` clearCredentials(); `
` AmazonTVMClient tvm =`
` new AmazonTVMClient(this.sharedPreferences,`
` PropertyLoader.getInstance().getTokenVendingMachineURL(),`
` PropertyLoader.getInstance().getAppName(),`
` PropertyLoader.getInstance().useSSL() );`
` if ( ableToGetToken.requestWasSuccessful() ) {`
` ableToGetToken = tvm.getToken(); `
` }`
` }`
` if (ableToGetToken.requestWasSuccessful() && s3Client == null ) { `
` AWSCredentials credentials =`
` AmazonSharedPreferencesWrapper.getCredentialsFromSharedPreferences(`
` this.sharedPreferences );`
` s3Client = new AmazonS3Client( credentials );`
` s3Client.setRegion(Region.getRegion(Regions.CN_NORTH_1));`
` }`
` return ableToGetToken;`
`}`
`public Response getToken() {`
` String uid = AmazonSharedPreferencesWrapper.getUidForDevice( this.sharedPreferences );`
` String key = AmazonSharedPreferencesWrapper.getKeyForDevice( this.sharedPreferences );`
` Request getTokenRequest = new GetTokenRequest( this.endpoint, this.useSSL, uid, key );`
` ResponseHandler handler = new GetTokenResponseHandler( key );`
` GetTokenResponse getTokenResponse =`
` (GetTokenResponse)this.processRequest( getTokenRequest, handler ); `
` if ( getTokenResponse.requestWasSuccessful() ) {`
` AmazonSharedPreferencesWrapper.storeCredentialsInSharedPreferences(`
` this.sharedPreferences, `
` getTokenResponse.getAccessKey(), `
` getTokenResponse.getSecretKey(), `
` getTokenResponse.getSecurityToken(), `
` getTokenResponse.getExpirationDate() );`
` }`
` return getTokenResponse;`
`}`
** 移动客户端和 TVM 系统安全通信设计 **
开发者如果需要移动客户端应用在非安全的互联网上直接与 TVM 系统通信,比如直接使用 HTTP 而非 HTTPS 发送登录请求和接收临时安全证书,开发者还需要自己实现一定程度的消息加密解密过程,避免敏感信息比如会话 Key 或临时安全证书内容在传输过程中被泄密。
** 演示效果 **
用户通过手机客户端注册新账号,执行完成登录操作后,就可以上传,查看和删除属于自己的图片文件。上传文件过程支持用户输入文本内容由系统自动产生上传文件和直接从手机客户端选择需要上传的图片文件。
!
通过查看 AWS S3 存储桶内容,我们可以看到每个用户上传的图片或文本文件都保存在属于该用户自己的 S3 存储桶路径下面:
!
在 TVM 系统 DynamoDB 用户数据库的用户表中保存了用户名、用户动态 ID 和加密的用户密码信息:
!
在 TVM 系统 DynamoDB 用户数据库的设备表中保存了用户的会话 Key 值:
!
### 例子源码
TVM 系统服务器端源码
https://s3.cn-north-1.amazonaws.com.cn/mwpublic/projects/tvm/TVMServer.zip
安卓客户端源码
https://s3.cn-north-1.amazonaws.com.cn/mwpublic/projects/tvm/TVMAndroidClient.zip
### 参考链接
http://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/IAM_Introduction.html
http://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_roles.html
http://docs.aws.amazon.com/zh_cn/STS/latest/UsingSTS/Welcome.html
http://docs.aws.amazon.com/zh_cn/STS/latest/APIReference/Welcome.html
### 敬请关注
在移动应用设计开发过程中,开发者除了完全靠自己开发实现用户注册和管理功能外,还可以考虑与主流社交媒体身份提供商实现联合身份认证,让已经拥有这些社交媒体身份提供商注册账号的用户能够顺利访问其移动应用。 AWS Cognito 服务已经支持与 Google、Facebook、Twitter 或 Amazon 等国际知名社交媒体身份提供商的联合身份认证。后续我们会陆续推出如何与微信、QQ 和微博等国内主要社交媒体的联合身份认证方案探讨。
** 作者介绍:**
!
蒙维
亚马逊 AWS 解决方案架构师,负责基于 AWS 的云计算方案架构咨询和设计,有超过十年以上电信行业和移动互联网行业复杂应用系统架构和设计经验, 主要擅长分布式和高可用软件系统架构设计,移动互联网应用解决方案设计,研发机构 DevOps 最佳实施过程。

本文转载自 AWS 技术博客。

原文链接:
https://amazonaws-china.com/cn/blogs/china/token-vending-machine/

欲了解 AWS 的更多信息,请访问【AWS 技术专区】

评论

发布