Skip to content

Commit

Permalink
Merge pull request #75 from AlibabaCloudLandingZone/solution-tvm/0.0.1
Browse files Browse the repository at this point in the history
solution-tvm/0.0.1
  • Loading branch information
wibud authored Sep 10, 2024
2 parents 3d7edfb + 559af93 commit 0d3569a
Show file tree
Hide file tree
Showing 16 changed files with 775 additions and 0 deletions.
32 changes: 32 additions & 0 deletions solution/solution-tvm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### IntelliJ IDEA ###
.idea/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
97 changes: 97 additions & 0 deletions solution/solution-tvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 通过TVM实现临时凭证的获取和使用

在典型的服务端和客户端架构下,客户端如果需要访问阿里云,比如基于OSS的用户文件上传下载、使用SLS记录客户端日志等等场景,常见的方式是在客户端使用RAM用户的访问密钥(AccessKey)来完成,但是在客户端中使用长期有效的访问密钥,可能会导致访问密钥泄露,进而引起安全问题。

本方案介绍了一种在客户端获取并使用STS临时凭证的方式,通过STS临时凭证访问阿里云,无需透露长期AccessKey,减少密钥泄露的风险。同时,通过会话权限策略,可以进行精细化的权限管控,避免越权问题。

这里针对本方案提供了Java SpringBoot的代码示例,帮助您快速完成应用改造,减少开发和部署的复杂度。

## 使用步骤

### 目录结构说明

```
.
└── code-example
└── java
└── spring-boot # Java SpringBoot示例代码
```

### Java示例代码

```
java/spring-boot/src/main
├── java/org/example
│ ├── Application.java
│ ├── config
│ │ ├── CredentialConfig.java # 初始化凭据客户端
│ │ └── StsClientConfig.java # 初始化STS客户端
│ ├── controller
│ │ └── TvmController.java
│ └── service
│ ├── StsTokenVendor.java # 获取STS Token
│ ├── TokenVendingMachine.java
│ └── policy
│ ├── PolicyGenerator.java # 生成Session Policy
│ └── PolicyTemplateLoader.java # 加载权限模版
└── resources
├── application.properties
├── policy-templates # 权限模版
│ ├── OssTemplate.json
│ └── SlsTemplate.json
└── static # 前端示例
├── oss.html # OSS示例
└── sls.html # SLS示例
```

#### 环境要求

该示例代码需要在ECS环境中执行,执行前,请确保运行环境中已配置好Java和Maven。

1. Java Development Kit (JDK):确保已安装Java 8或更高版本。
2. Apache Maven:确保已安装Maven 3.6.0或更高版本。

运行以下命令来检查Java安装:

```bash
java -version
```

运行以下命令来检查Maven安装:

```bash
maven -version
```

#### 本地运行

1. 首先您需要配置凭证信息,建议您通过环境变量进行配置:

```
ALIBABA_CLOUD_ACCESS_KEY_ID=<您的AccessKey ID>;ALIBABA_CLOUD_ACCESS_KEY_SECRET=<您的AccessKey Secret>
```
> 您也可以将该项目部署到阿里云上,强烈建议您使用临时凭证来代替固定AccessKey。
2. 接着您需要进行应用配置,打开 `resources/application.properties` 进行如下配置:
```
# 服务启动端口个
server.port = 7001
# 地域,以杭州地域为例
region.id=cn-hangzhou
# 请填写要扮演的业务RAM角色ARN,格式为acs:ram::${账号 ID}:role/${角色名称}
role.arn=
# 请填写OSS Bucket名称,示例中会从该Bucket中上传下载文件
oss.bucket=
# 请填写SLS Project名称
sls.project=
```
同时,在`resources/static/oss.html`和`resources/static/sls.html`中配置对应的信息。
3. 启动`Application.java`,浏览器打开`resources/static/oss.html`和`resources/static/sls.html`体验Web端示例
81 changes: 81 additions & 0 deletions solution/solution-tvm/code-example/java/spring-boot/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<groupId>org.example</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
<exec.cleanupDaemonThreads>false</exec.cleanupDaemonThreads>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
<version>1.15</version>
</dependency>

<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.51</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>

<!--1.0 sdk-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.7.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-sts</artifactId>
<version>3.1.2</version>
</dependency>

<!--2.0 sdk-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>sts20150401</artifactId>
<version>1.1.4</version>
</dependency>

<!-- Requires: version >= 0.3.4 -->
<!-- 推荐使用最新版本 -->
<!--获取所有已发布的版本列表,请参见https://github.com/aliyun/credentials-java/blob/master/ChangeLog.txt-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>credentials-java</artifactId>
<version>0.3.5</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.example.config;

import com.aliyun.credentials.Client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CredentialConfig {

// 初始化凭据客户端,Credential SDK Client 应该是单例,不要每次请求都重新 new 一个,避免内存泄露
// 借助Credentials工具的默认凭据链,您可以用同一套代码,通过程序之外的配置来控制不同环境下的凭据获取方式
// 当您在初始化凭据客户端不传入任何参数时,Credentials工具将会尝试按照如下顺序查找相关凭据信息(优先级由高到低):
// 1. 使用系统属性
// 2. 使用环境变量
// 3. 使用OIDC RAM角色
// 4. 使用配置文件
// 5. 使用ECS实例RAM角色(需要通过环境变量 ALIBABA_CLOUD_ECS_METADATA 指定 ECS 实例角色名称;通过环境变量 ALIBABA_CLOUD_ECS_IMDSV2_ENABLE=true 开启在加固模式下获取STS Token)
// https://help.aliyun.com/zh/sdk/developer-reference/v2-manage-access-credentials#3ca299f04bw3c
// 要使用默认凭据链,初始化 Client 时,必须使用空的构造函数,不能配置 Config 入参
@Bean(name = "credentialClient")
Client getCredentialClient() {
return new Client();
}

// 除了使用上面的默认凭据链,您也可以在代码中显式配置,来初始化凭据客户端
// 如下所示,可以进行显式配置,以ECS实例角色为例
//@Bean(name = "credentialClient")
//Client getCredentialClient() {
// Config config = new Config()
// .setType("ecs_ram_role")
// // 选填,该ECS实例角色的角色名称,不填会自动获取,建议加上以减少请求次数
// .setRoleName("<请填写ECS实例角色的角色名称>")
// // 在加固模式下获取STS Token,强烈建议开启
// .setEnableIMDSv2(true);
// return new Client(config);
//}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StsClientConfig {

@Value("${region.id}")
String regionId;

@Autowired
com.aliyun.credentials.Client credentialClient;

// 初始化阿里云 V2 版本 STS 的 SDK 客户端
// SDK Client 应该是单例,不要每次请求都重新 New 一个,避免内存泄露
@Bean(name = "stsClient")
com.aliyun.sts20150401.Client getStsClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setCredential(credentialClient)
.setRegionId(regionId);
return new com.aliyun.sts20150401.Client(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.example.controller;

import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import org.example.service.TokenVendingMachine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TvmController {

@Autowired
TokenVendingMachine tokenVendingMachine;

// 测试示例使用,配置了 CORS 跨域
@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 86400)
@GetMapping("/vendToken")
public AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials vendToken(@RequestHeader("Custom-Identity") String identity) {
// 根据自身应用需要,进行请求的合法性校验
// 本示例只是演示使用,不会对请求身份等进行合法性校验
return tokenVendingMachine.vendToken(identity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.example.service;

import com.aliyun.sts20150401.models.AssumeRoleRequest;
import com.aliyun.sts20150401.models.AssumeRoleResponse;
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.tea.TeaUnretryableException;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.Builder;

@Builder
public class StsTokenVendor {

private final com.aliyun.sts20150401.Client stsClient;

/**
* 要扮演的RAM角色ARN,acs:ram::${账号 ID}:role/${角色名称}
*/
private final String roleArn;

/**
* 角色会话名称
*/
private final String roleSessionName;

/**
* 会话权限策略,可以进一步缩小权限,进行精细化管控
*/
private final String sessionPolicy;

/**
* STS Token有效期,单位:秒
*/
private final Long durationSeconds;

public AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials vendToken() {
RuntimeOptions runtimeOptions = new RuntimeOptions()
// 开启自动重试机制
.setAutoretry(true)
// 设置自动重试次数,默认3次
.setMaxAttempts(3);
AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest()
.setRoleArn(roleArn)
.setRoleSessionName(roleSessionName)
.setPolicy(sessionPolicy)
.setDurationSeconds(durationSeconds);
try {
AssumeRoleResponse assumeRoleResponse = stsClient.assumeRoleWithOptions(assumeRoleRequest, runtimeOptions);
return assumeRoleResponse.getBody().getCredentials();
} catch (TeaUnretryableException e) {
// 该异常主要是因为网络问题造成,一般是网络问题达到最大重试次数后抛出,可以通过exception.getLastRequest来查询错误发生时的请求信息。
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
e.printStackTrace();
// 打印错误信息
System.out.println(e.getMessage());
// 打印请求记录
System.out.println(e.getLastRequest());
throw e;
} catch (TeaException e) {
// 在SDK的请求中主要以业务报错为主的异常
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
e.printStackTrace();
// 打印错误码
System.out.println(e.getCode());
// 打印错误信息,错误信息中包含 RequestId
System.out.println(e.getMessage());
// 打印服务端返回的具体错误内容
System.out.println(e.getData());
throw e;
} catch (Exception e) {
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
Loading

0 comments on commit 0d3569a

Please sign in to comment.