移动应用使用临时安全令牌访问阿里云

本文介绍移动应用如何使用RAM角色的临时安全令牌(STS Token)访问阿里云资源。

背景信息

企业A开发了一款移动应用(App),并购买了对象存储(OSS)服务。App需要直连OSS上传或下载数据,但是App运行在用户自己的移动设备上,这些设备不受企业A的控制。

企业A有如下要求:

  • 直传数据:企业A不希望所有App都通过企业的服务端应用服务器(Application Server)来进行数据中转,而希望能够直连OSS上传或下载数据。

  • 安全管控:企业A不希望将访问密钥(AccessKey)保存到移动设备中,因为移动设备是归属于用户控制,属于不可信的运行环境。

  • 风险控制:企业A希望将风险控制到最小,每个App直连OSS时都必须拥有最小的访问权限且访问时效需要很短。

解决方案

当移动应用(App)直连OSS上传或下载数据时,App需要向应用服务器申请访问凭证。应用服务器以RAM用户身份扮演RAM角色,调用STS API AssumeRole接口获取临时安全令牌,并将临时安全令牌传递给App,App使用临时安全令牌访问OSS。

移动设备应用访问阿里云

  1. App向应用服务器申请访问凭证。

  2. 使用阿里云账号A创建一个RAM角色,并为RAM角色授予合适的权限。

    具体操作,请参见创建RAM角色并授权

  3. 使用阿里云账号A为应用服务器创建一个RAM用户,并允许应用服务器以RAM用户身份扮演该RAM角色。

    具体操作,请参见创建RAM用户并允许扮演RAM角色

  4. 应用服务器通过调用STSAssumeRole接口获取RAM角色的临时安全令牌。

    具体操作,请参见应用服务器获取临时安全令牌

  5. 应用服务器可以进一步限制临时安全令牌的权限,以更精细地控制每个App的权限。

    具体操作,请参见限制临时安全令牌的权限

  6. App需要直连OSS上传或下载数据时,可以使用临时安全令牌访问OSS进行数据直传。

    具体操作,请参见App使用临时安全令牌并访问OSS

创建RAM角色并授权

假设阿里云账号A的账号ID123456789012****

  1. 使用阿里云账号A创建可信实体为云账号RAM角色oss-objectmanager

    说明

    创建RAM角色时选择当前云账号作为信任主体,即只允许阿里云账号A下的RAM用户来扮演该RAM角色。

    具体操作,请参见创建可信实体为阿里云账号的RAM角色

    RAM角色创建成功后,在角色基本信息页面可以查看到该RAM角色的ARN和信任策略。

    • RAM角色的ARNacs:ram::123456789012****:role/oss-objectmanager

    • RAM角色的信任策略如下。

      说明

      以下策略表示只允许阿里云账号A下的RAM用户来扮演RAM角色。

      {
        "Statement": [
          {
            "Action": "sts:AssumeRole",
            "Effect": "Allow",
            "Principal": {
              "RAM": [
                "acs:ram::123456789012****:root"
              ]
            }
          }
        ],
        "Version": "1"
      }
  2. RAM角色授权。为RAM角色oss-objectmanager授予OSS的管理权限AliyunOSSFullAccess

    具体操作,请参见RAM角色授权

创建RAM用户并允许扮演RAM角色

  1. 使用阿里云账号A为应用服务器创建RAM用户appserver

    具体操作,请参见创建RAM用户

  2. 为创建好的RAM用户授予AliyunSTSAssumeRoleAccess权限,即允许RAM用户扮演RAM角色。

    具体操作,请参见RAM用户授权

应用服务器获取临时安全令牌

  1. 应用服务器使用RAM用户的访问密钥(AccessKey)调用STSAssumeRole接口。

    说明
    • 使用阿里云CLI前需要在应用服务器中安装CLI工具,并使用RAM用户的AccessKey配置相应凭证。具体操作,请参见STS CLI参考

    • 当前CLI示例中未指定Policy参数,因此返回的临时安全令牌将拥有RAM角色oss-objectmanager的所有权限。您也可以额外限制临时安全令牌的权限,更多信息,请参见限制临时安全令牌的权限

    使用阿里云CLI调用AssumeRole的示例如下:

    aliyun sts AssumeRole --RoleArn 'acs:ram::123456789012****:role/oss-objectmanager' --RoleSessionName 'client-001'
  2. STS服务将临时安全令牌返回给应用服务器。返回的临时安全令牌中包含AccessKeyIdAccessKeySecretSecurityToken

    返回示例如下:

    {
         "AssumedRoleUser": {
             "AssumedRoleId": "391578752573****:client-001",
             "Arn": "acs:ram::123456789012****:role/oss-objectmanager/client-001"
         }, 
         "Credentials": {
             "AccessKeySecret": "yourAccessKeySecret",
             "SecurityToken": "yourSecurityToken",
             "Expiration": "2016-01-13T15:02:37Z",
             "AccessKeyId": "yourAccessKeyId"
         }, 
         "RequestId": "E1779AAB-E7AF-47D6-A9A4-53128708B6CE"
     }
    说明

    SecurityToken过期时间较短。如果需要一个较长的过期时间,应用服务器需要重新颁发临时安全令牌,例如:每隔1800秒颁发一次。

限制临时安全令牌的权限

您可以使用Policy参数来根据用户或设备限制不同临时安全令牌的权限,避免越权风险。

以下示例表示:只允许下载sample-bucket/2015/01/01/*.jpg

  • 请求示例

    aliyun sts AssumeRole --RoleArn 'acs:ram::123456789012****:role/oss-objectmanager' --RoleSessionName 'client-002' --Policy '{"Version":"1", "Statement": [{"Effect":"Allow", "Action":"oss:GetObject", "Resource":"acs:oss:*:*:sample-bucket/2015/01/01/*.jpg"}]}'
    说明

    临时安全令牌过期时间默认值为3600秒,通过DurationSeconds参数可以设置过期时间。更多信息,请参见AssumeRole - 获取扮演角色的临时身份凭证

  • 返回示例

    {
       "AssumedRoleUser": {
           "AssumedRoleId": "391578752573****:client-002",
           "Arn": "acs:ram::123456789012****:role/oss-objectmanager/client-002"
       },
       "Credentials": {
           "AccessKeySecret": "yourAccessKeySecret",
           "SecurityToken": "yourSecurityToken",
           "Expiration": "2016-01-13T15:03:39Z",
           "AccessKeyId": "yourAccessKeyId"
       }, 
       "RequestId": "98835D9B-86E5-4BB5-A6DF-9D3156ABA567"
    }

App使用临时安全令牌访问OSS

  1. 应用服务器将临时安全令牌传递给App。

  2. App使用临时安全令牌访问OSS。

    本示例为您演示如何在Android应用中集成阿里云OSS Android SDK,并通过临时安全令牌访问OSS,实现将远程文件下载至本地设备的功能。示例代码如下:

    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    String endpoint = "yourEndpoint";
    // yourRegion填写Bucket所在地域。以华东1(杭州)为例,region填写为cn-hangzhou。
    String region = "yourRegion";
    
    // 从STS服务获取的临时访问密钥(AccessKeyIDAccessKeySecret)。
    String accessKeyId = "yourAccessKeyId";
    String accessKeySecret = "yourAccessKeySecret";
    // 从STS服务获取的临时安全令牌(SecurityToken)。
    String securityToken = "yourSecurityToken";
    
    OSSCredentialProvider credentialProvider = new OSSStsTokenCredentialProvider(accessKeyId, accessKeySecret, securityToken);
    ClientConfiguration config = new ClientConfiguration();
    config.setSignVersion(SignVersion.V4);
    // 创建OSSClient实例。
    OSSClient oss = new OSSClient(getApplicationContext(), endpoint, credentialProvider);
    oss.setRegion(region);
    // 构造下载文件请求。
    GetObjectRequest get = new GetObjectRequest("sample-bucket", "2015/01/01/grass.jpg");
    
    OSSAsyncTask task = oss.asyncGetObject(get, new OSSCompletedCallback<GetObjectRequest, GetObjectResult>() {
        @Override
        public void onSuccess(GetObjectRequest request, GetObjectResult result) {
            // 请求成功。
            Log.d("asyncGetObject", "DownloadSuccess");
            Log.d("Content-Length", "" + result.getContentLength());
            
            try (InputStream inputStream = result.getObjectContent()) {
                byte[] buffer = new byte[2048];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                // 处理数据
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        // GetObject请求成功,返回GetObjectResult。GetObjectResult包含一个输入流的实例。您需要自行处理返回的输入流。
        public void onFailure(GetObjectRequest request, ClientException clientExcepion, ServiceException serviceException) {
            // 请求异常。
            if (clientExcepion != null) {
                // 本地异常,如网络异常等。
                clientExcepion.printStackTrace();
            }
            if (serviceException != null) {
                // 服务异常。
                Log.e("ErrorCode", serviceException.getErrorCode());
                Log.e("RequestId", serviceException.getRequestId());
                Log.e("HostId", serviceException.getHostId());
                Log.e("RawMessage", serviceException.getRawMessage());
            }
        }
    });
    // 取消任务。
    // task.cancel(); 
    // 等待任务完成。
    // task.waitUntilFinished();