亚马逊AWS官方博客

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 方法提供了多个参数,可以灵活的设置分配的临时安全证书的各种特性。我们这里主要介绍演示项目用到的几个重要参数。

 
名称 类型 必填 含义
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://thinkwithwp.com/articles/4611615499399490

https://thinkwithwp.com/code/Java/8872061742402990

http://thinkwithwp.com/code/4598681430241367

http://docs.thinkwithwp.com/zh_cn/IAM/latest/UserGuide/IAM_Introduction.html

http://docs.thinkwithwp.com/zh_cn/IAM/latest/UserGuide/id_roles.html

http://docs.thinkwithwp.com/zh_cn/STS/latest/UsingSTS/Welcome.html

http://docs.thinkwithwp.com/zh_cn/STS/latest/APIReference/Welcome.html

敬请关注

在移动应用设计开发过程中,开发者除了完全靠自己开发实现用户注册和管理功能外,还可以考虑与主流社交媒体身份提供商实现联合身份认证,让已经拥有这些社交媒体身份提供商注册账号的用户能够顺利访问其移动应用。AWS Cognito服务已经支持与Google、Facebook、Twitter 或 Amazon等国际知名社交媒体身份提供商的联合身份认证。后续我们会陆续推出如何与微信、QQ和微博等国内主要社交媒体的联合身份认证方案探讨。

作者介绍:

蒙维

亚马逊AWS解决方案架构师,负责基于AWS的云计算方案架构咨询和设计,有超过十年以上电信行业和移动互联网行业复杂应用系统架构和设计经验,主要擅长分布式和高可用软件系统架构设计,移动互联网应用解决方案设计,研发机构DevOps最佳实施过程。