实战:用 Java 模拟登录阿里云控制台,爬取没有 OpenAPI 的数据

关键词:阿里云 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.5java.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 角色需要:

  1. 信任策略:允许当前账号 AssumeRole
  2. 权限策略 :至少有访问目标控制台的权限(如 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;
}

注意点

  1. 把临时凭证作为 query 参数明文传递,所以一定要走 HTTPS。
  2. 响应字段大小写有时不一致(SigninToken vs signinToken),代码做了兼容。

拿到 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 就是一个迷你浏览器


到这里,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;
    }
}

怎么找到这种私有接口?

非常朴素的办法:

  1. 打开阿里云控制台对应页面
  2. 按 F12 打开浏览器 DevTools
  3. 切到 Network 标签
  4. 刷新页面 / 点操作按钮
  5. 过滤 XHR / Fetch
  6. 找返回 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);
}

SecurityTokenSigninToken 里包含 /+= 等字符,必须 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 操作

八、写在最后:一些注意事项

  1. 合规性 :爬控制台数据虽然技术上可行,但要确保只爬自己账号下的数据,且符合阿里云用户协议。不要做反向工程别人的账号。
  2. 稳定性 :控制台接口随时可能变(字段、URL、参数),要做好降级和告警
  3. 效率:能用官方 OpenAPI 就用 OpenAPI;只有在 OpenAPI 没覆盖的场景才考虑控制台爬虫。
  4. 凭证管理:主账号 AK 不要硬编码,用环境变量或 KMS。
  5. 日志脱敏 :日志里不要打印 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。

相关推荐
程序员二叉1 小时前
【Java】 面试核心合集:BigDecimal、缓存池、多态、反射全解析
java·缓存·面试
Full Stack Developme1 小时前
SpringMVC multipart 文件上传
java·开发语言
西凉的悲伤1 小时前
Spring Security + JWT 登录认证完整实践指南
java·后端·spring·spring security·jwt
晚笙coding1 小时前
从零讲透 LangChain 输出格式化:让模型真的“能用”
java·开发语言·langchain
奋斗的小方1 小时前
Java进阶篇1-1:异常
java·开发语言·python
码语智行1 小时前
行政区划 ZIP 导入(importZip)
java
何中应1 小时前
Nexus如何设置端口号
java·服务器·maven·nexus
思麟呀1 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
Full Stack Developme1 小时前
Hutool CollUtil 教程
java·开发语言·windows·python