关键词:阿里云 STS、AssumeRole、Federation Login、Java HttpClient、CookieManager、控制台爬虫
一、背景
在和阿里云对接的业务里,经常会遇到一个尴尬的场景:
你在控制台页面里能看到一份数据,但官方 OpenAPI 文档里根本找不到对应的接口。
例如:
- 云市场买完商品后生成的 License Code 列表
- 某些只在前端页面展示的统计、明细数据
- 一些后台运营页才有的字段
这些数据其实是控制台前端通过 Ajax 请求一个 *.console.aliyun.com/xxx.json 接口拿到的。只要我们能让程序"伪装成已登录的用户"去请求这些接口,就能拿到数据。
本文用一段真实可运行的 Java 代码(基于 Java 17,使用 JDK 内置的 java.net.http.HttpClient),完整拆解这个"控制台爬虫"的全流程:
STS AssumeRole → GetSigninToken → Federation Login → 调控制台 JSON 接口
二、整体架构
┌──────────────┐ ┌──────────────┐ ┌───────────────┐ ┌─────────────────┐
│ Step 1: STS │ ──→ │ Step 2: │ ──→ │ Step 3: │ ──→ │ Step 4: │
│ AssumeRole │ │ GetSigninToken│ │ Federation │ │ 调控制台 JSON │
│ 拿临时凭证 │ │ 换登录令牌 │ │ Login 拿Cookie│ │ 抓取业务数据 │
└──────────────┘ └──────────────┘ └───────────────┘ └─────────────────┘
阿里云 SDK HTTP GET HTTP GET + 302 HTTP GET + Cookie
每一步都对应阿里云一种不同的安全机制:
| 步骤 | 目的 | 输入 | 输出 |
|---|---|---|---|
| Step 1 | 用主账号 AK 换临时凭证 | 主账号 AccessKey | 临时 AK/SK/SecurityToken |
| Step 2 | 把临时凭证换成"登录入场券" | 临时凭证 | SigninToken |
| Step 3 | 用入场券完成 Web 登录 | SigninToken | 一组 Cookie |
| Step 4 | 用 Cookie 调控制台私有接口 | Cookie | 业务 JSON 数据 |
三、依赖与配置
Maven 依赖
xml
<!-- 阿里云 STS SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>sts20150401</artifactId>
<version>1.1.7</version>
</dependency>
<!-- Jackson 解析 JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
本项目使用 Java 17 + Spring Boot 3.2.5 。java.net.http.HttpClient 是 JDK 内置组件(自 Java 11 起提供,Java 17 直接可用),不需要额外引入 OkHttp 或 Apache HttpClient。record 语法也是 Java 14+ 特性,在 Java 17 中是正式语法。
必要的账号信息
properties
aliyun.market.access-key-id=xxxxx # 主账号或子账号 AK
aliyun.market.access-key-secret=xxxxxx # 对应 SK
aliyun.market.payer-account-id=xxxxxx # 阿里云账号 UID
aliyun.market.ram-role-name=marketplace-automation # 提前在 RAM 控制台创建的角色名
RAM 角色需要:
- 信任策略:允许当前账号 AssumeRole
- 权限策略 :至少有访问目标控制台的权限(如
AliyunMarketFullAccess)
四、四步完整实现
Step 1:STS AssumeRole --- 拿到临时凭证
为什么不能直接用主账号 AK?
主账号 AK 权限太大,泄露后果严重。阿里云推荐用 RAM 角色 + STS 临时凭证:
- 临时凭证默认有效期 1 小时
- 权限被角色策略限制
- 即使泄露影响也可控
java
private StsCredentials assumeRole() throws Exception {
Config config = new Config();
config.setAccessKeyId(properties.getAccessKeyId());
config.setAccessKeySecret(properties.getAccessKeySecret());
config.setEndpoint("sts.aliyuncs.com");
Client client = new Client(config);
String roleArn = "acs:ram::" + properties.getPayerAccountId()
+ ":role/" + properties.getRamRoleName();
AssumeRoleRequest request = new AssumeRoleRequest();
request.setRoleArn(roleArn);
request.setRoleSessionName("marketplace-get-license");
AssumeRoleResponse response = client.assumeRole(request);
var creds = response.getBody().getCredentials();
return new StsCredentials(
creds.getAccessKeyId(),
creds.getAccessKeySecret(),
creds.getSecurityToken()
);
}
public record StsCredentials(String accessKeyId, String accessKeySecret, String securityToken) {}
调用成功后会拿到 3 个值:
AccessKeyId = STS.NTxxxxxxx
AccessKeySecret = xxxxxxxxxx
SecurityToken = CAIS+gFjxxxxxxxxxx ← 这个是 STS 特有的
Step 2:GetSigninToken --- 把 API 凭证换成"登录入场券"
为什么还要再换?
临时凭证(AK/SK/SecurityToken)是给 API 用的(要做签名)。但浏览器登录靠 Cookie,两者格式完全不同。
阿里云提供了一个 Federation 接口,专门做这个"API 凭证 → Web 登录令牌"的转换:
https://signin.alibabacloud.com/federation?Action=GetSigninToken
java
private String getSigninToken(HttpClient httpClient, StsCredentials creds) throws Exception {
String url = "https://signin.alibabacloud.com/federation"
+ "?Action=GetSigninToken"
+ "&AccessKeyId=" + urlEncode(creds.accessKeyId())
+ "&AccessKeySecret=" + urlEncode(creds.accessKeySecret())
+ "&SecurityToken=" + urlEncode(creds.securityToken());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("GetSigninToken failed: HTTP " + response.statusCode());
}
JsonNode json = objectMapper.readTree(response.body());
String token = json.path("SigninToken").asText(null);
if (token == null || token.isBlank()) {
token = json.path("signinToken").asText(null); // 注意大小写兼容
}
if (token == null || token.isBlank()) {
throw new RuntimeException("SigninToken empty. Response: " + response.body());
}
return token;
}
注意点:
- 把临时凭证作为 query 参数明文传递,所以一定要走 HTTPS。
- 响应字段大小写有时不一致(
SigninTokenvssigninToken),代码做了兼容。
Step 3:Federation Login --- 拿到 Cookie
拿到 SigninToken 后,再调一次 Federation:
https://signin.alibabacloud.com/federation?Action=Login
&LoginUrl=https://www.aliyun.com
&SigninToken=xxx
&Destination=https://market-intl.console.aliyun.com/
服务器会在响应头里返回一堆 Set-Cookie,然后 302 跳转到 Destination。
java
private void login(HttpClient httpClient, String signinToken) throws Exception {
String url = "https://signin.alibabacloud.com/federation"
+ "?Action=Login"
+ "&LoginUrl=" + urlEncode("https://www.aliyun.com")
+ "&SigninToken=" + urlEncode(signinToken)
+ "&Destination=" + urlEncode("https://market-intl.console.aliyun.com/");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 302) {
throw new RuntimeException("Federation login failed: HTTP " + response.statusCode());
}
}
关键来了:Cookie 怎么自动保存?
看 HttpClient 的构建:
java
private HttpClient createHttpClient() {
return HttpClient.newBuilder()
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
两个关键配置:
| 配置 | 作用 |
|---|---|
CookieManager(ACCEPT_ALL) |
自动从响应头 Set-Cookie 保存 Cookie,下次请求同域名时自动带上 |
followRedirects(NORMAL) |
自动跟 302 跳转。很重要 ------ 阿里云登录会跳 3~4 次,每跳一次种几个 Cookie |
只要这两个配置开了,JDK 自带的 HttpClient 就是一个迷你浏览器。
Step 4:用 Cookie 调控制台私有接口
到这里,httpClient 已经"登录成功",下面就可以直接请求控制台数据了:
java
private List<LicenseItem> fetchLicenseList(HttpClient httpClient, String productCode) throws Exception {
String url = "https://market-intl.console.aliyun.com/license/list.json"
+ "?productCode=" + urlEncode(productCode)
+ "&pageIndex=1"
+ "&pageSize=100";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Fetch license list failed: HTTP " + response.statusCode());
}
JsonNode json = objectMapper.readTree(response.body());
if (!json.path("success").asBoolean(false)) {
throw new RuntimeException("API returned failure: " + response.body());
}
JsonNode listNode = json.path("data").path("list");
if (listNode.isMissingNode() || !listNode.isArray()) {
return Collections.emptyList();
}
List<LicenseItem> items = new ArrayList<>();
for (JsonNode node : listNode) {
items.add(new LicenseItem(
node.path("licenseCode").asText(null),
node.path("status").asText(null),
node.path("activeUrl").asText(null),
node.path("startTime").asLong(0L),
node.path("endTime").asLong(0L)
));
}
return items;
}
public record LicenseItem(
String licenseCode,
String status,
String activeUrl,
long startTime,
long endTime
) {
/** 从 activeUrl 里提取 instanceId,例如 "#/bizinfo/5000003205685" → "5000003205685" */
public String instanceId() {
if (activeUrl == null || activeUrl.isBlank()) return null;
String url = activeUrl.startsWith("#") ? activeUrl.substring(1) : activeUrl;
int lastSlash = url.lastIndexOf('/');
return lastSlash >= 0 ? url.substring(lastSlash + 1) : url;
}
}
怎么找到这种私有接口?
非常朴素的办法:
- 打开阿里云控制台对应页面
- 按 F12 打开浏览器 DevTools
- 切到 Network 标签
- 刷新页面 / 点操作按钮
- 过滤 XHR / Fetch
- 找返回 JSON 的那条请求,右键 → "Copy as cURL"
复制下来的 URL、Header、参数就是你要复刻的目标。
五、串起来:一次完整调用
java
public List<LicenseItem> fetchLicenseCodes(String productCode) {
try {
// Step 1: 拿临时凭证
StsCredentials creds = assumeRole();
// Step 2-4: 共用同一个 HttpClient(Cookie 才能延续)
HttpClient httpClient = createHttpClient();
// Step 2: 换 SigninToken
String signinToken = getSigninToken(httpClient, creds);
// Step 3: Federation Login,把 Cookie 种进 httpClient
login(httpClient, signinToken);
// Step 4: 用 Cookie 调控制台接口
return fetchLicenseList(httpClient, productCode);
} catch (Exception e) {
throw new RuntimeException("Failed to fetch license codes: " + productCode, e);
}
}
最关键一点 :Step 2--4 必须用同一个 HttpClient 实例,因为 Cookie 是绑定在它的 CookieManager 里的。
六、几个工程化技巧
1. SSL 证书校验开关(仅限调试)
在某些抓包调试场景下(例如用 Charles / Fiddler 解 HTTPS),需要关掉证书校验:
java
private SSLContext createInsecureSslContext() throws Exception {
TrustManager[] trustAll = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new SecureRandom());
return ctx;
}
生产环境务必关闭这个开关,否则容易被中间人攻击。
2. URL 参数编码
java
private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
SecurityToken、SigninToken 里包含 /、+、= 等字符,必须 URL 编码。
3. JSON 字段大小写兼容
java
String token = json.path("SigninToken").asText(null);
if (token == null) token = json.path("signinToken").asText(null);
阿里云有些接口返回字段大小写不一致,做兜底。
4. 临时凭证过期
STS 凭证默认 1 小时过期。最简单的策略 就是每次需要数据时都从 Step 1 重新走一遍。这样虽然多 3 次 HTTP,但无状态、可靠、易调试。
如果调用频繁,可以缓存:
java
if (cachedCreds != null && cachedCreds.expiresAt().isAfter(Instant.now().plusSeconds(60))) {
return cachedCreds;
}
七、爬虫开发的通用范式总结
从这个案例可以提炼出一套通用的"控制台爬虫"开发范式:
| 步骤 | 做什么 | 工具 |
|---|---|---|
| 1. 找 API | 先看有没有 OpenAPI,能用就别爬 | 阿里云文档 / SDK |
| 2. 抓包 | 没有 API 就 F12 抓控制台请求 | Chrome DevTools / Charles |
| 3. 复刻认证 | 弄明白登录流程,拿到 Cookie | STS + Federation |
| 4. 维持 Session | 用支持 Cookie 的 HttpClient | JDK HttpClient (Java 11+) / OkHttp |
| 5. 跟随重定向 | 登录链路常有多次 302 | followRedirects(NORMAL) |
| 6. 兼容解析 | 字段缺失 / 大小写 / 嵌套要做兜底 | Jackson path() API |
| 7. 失败重试 | 凭证过期要能自动续 | 重新走 Step 1 |
| 8. 二次解析 | 业务字段藏在 URL 里要逆向提取 | 正则 / String 操作 |
八、写在最后:一些注意事项
- 合规性 :爬控制台数据虽然技术上可行,但要确保只爬自己账号下的数据,且符合阿里云用户协议。不要做反向工程别人的账号。
- 稳定性 :控制台接口随时可能变(字段、URL、参数),要做好降级和告警。
- 效率:能用官方 OpenAPI 就用 OpenAPI;只有在 OpenAPI 没覆盖的场景才考虑控制台爬虫。
- 凭证管理:主账号 AK 不要硬编码,用环境变量或 KMS。
- 日志脱敏 :日志里不要打印
AccessKeySecret/SecurityToken/SigninToken。
附录:完整代码
完整可运行的 LicenseCrawlerClient.java 可参考本文示例代码组合而成,核心结构为:
java
@Component
public class LicenseCrawlerClient {
public List<LicenseItem> fetchLicenseCodes(String productCode) { /* Step 1-4 */ }
private StsCredentials assumeRole() { /* Step 1 */ }
private String getSigninToken(HttpClient client, StsCredentials creds) { /* Step 2 */ }
private void login(HttpClient client, String signinToken) { /* Step 3 */ }
private List<LicenseItem> fetchLicenseList(HttpClient client, String productCode) { /* Step 4 */ }
private HttpClient createHttpClient() { /* CookieManager + followRedirects */ }
public record StsCredentials(String accessKeyId, String accessKeySecret, String securityToken) {}
public record LicenseItem(String licenseCode, String status, String activeUrl, long startTime, long endTime) {}
}
最后一句话送给同样在和云厂商打交道的同学:
当文档说"没有",控制台说"有"的时候,F12 才是你最好的 SDK。