将RDS AI助手页面嵌入自建Web应用

更新时间:
复制 MD 格式

本教程介绍如何将阿里云RDS AI助手页面以iframe形式嵌入自建Web应用。该方案通过RAM、STS(Security Token Service)临时凭证和阿里云登录服务,生成一个免登录访问链接,实现在自有系统中无缝集成和使用RDS AI助手的功能。

背景信息

在企业内部的数据库管理平台或运维系统中,您可能希望集成RDS AI助手的能力,以提供智能诊断、SQL优化等功能。通过将RDS AI助手页面嵌入您的自建Web应用,可以实现以下目标:

  • 提升用户体验: 用户无需在您的应用和阿里云控制台之间频繁切换和登录,实现单点登录(SSO)的无缝体验。

  • 精细化权限管控: 可通过RAM角色为不同用户授予不同的访问权限,例如为开发人员授予只读权限,为DBA授予完整管理权限,确保操作安全可控。

访问流程

整体访问流程如下图所示,核心步骤包括获取临时凭证、换取登录令牌、构造并访问最终的免登录URL。image

  1. 用户登录自建Web平台。

  2. 自建Web平台的后端服务调用STS的AssumeRole接口,请求一个临时身份凭证。

  3. STS服务返回包含临时AccessKey(AK)、Secret和Token的凭证。

  4. 后端服务使用该临时凭证,调用阿里云登录服务的GetSigninToken接口。

  5. 登录服务返回一个用于单点登录的SigninToken。

  6. 后端服务使用此SigninToken和目标RDS AI助手页面的URL,构造一个最终的免登录访问链接,并返回给前端。前端即可通过此链接在iframe中或新标签页中打开RDS AI助手。

创建RAM用户、RAM角色并授权

创建RAM用户并授权

创建一个专用于调用STS服务的RAM用户,并授予其扮演角色的权限。

1. 创建后端专用RAM用户

  1. 登录RAM控制台。

  2. 在左侧导航栏,选择身份管理 > 用户。

  3. 在用户页面,单击创建用户。

  4. 在创建用户页面,配置用户名称等基本信息,在访问配置区域,选中使用永久AccessKey访问,便于后端服务通过RAM用户的AccessKeyId和AccessKeySecret调用STS。

  5. 在创建结果页面,查看并保存RAM用户的AccessKeyId和AccessKeySecret。具体操作请参见创建AccessKey。

重要
  • 务必在创建结果页面保存AccessKeyId和AccessKeySecret,页面关闭后无法再次查询。请妥善保管该凭证,后续后端脚本将使用它们调用STS服务。

  • AccessKeySecret只能保存在自建Web后端运行环境或密钥管理系统中,不要写入前端代码、Git仓库、文档截图、日志或工单。

  • 不建议使用主账号AccessKey做集成验证。主账号权限过大,无法验证真实客户最常见的RAM授权边界问题。

2. 为RAM用户授权AssumeRole

后端专用RAM用户需要具备扮演目标RAM角色的权限,您需要创建一个自定义权限策略并授权给该RAM用户。

  1. 在左侧导航栏,选择权限管理 > 权限策略。

  2. 在权限策略页面,单击创建权限策略。

  3. 在创建权限策略页面,选择脚本编辑页签,输入以下JSON策略内容:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "acs:ram::<account_id>:role/<ram_role_name>"
    }
  ]
}

替换规则:

  • <account_id>:阿里云主账号ID。

  • <ram_role_name>:后续创建的RAM角色名称,例如rdscopilot-console。

创建完成后,将该策略授权给后端专用RAM用户:

  1. 在权限策略列表中,单击刚创建的策略名称。

  2. 在策略详情页面,单击授权管理页签,然后单击新增授权。

  3. 在新增授权面板,资源范围选择账号级别。

  4. 在授权主体区域,勾选上一步创建的后端专用RAM用户。

  5. 单击确认新增授权。具体操作请参见管理RAM用户的权限。

创建RAM角色并授权

创建一个RAM角色,您的Web应用将通过AssumeRole扮演该角色来访问RDS AI助手。最终用户访问页面时,将拥有此角色所具备的权限。

1. 创建可信实体为阿里云账号的RAM角色

在RAM控制台创建RAM角色,操作步骤如下:

  1. 在左侧导航栏,选择身份管理 > 角色。

  2. 在角色页面,单击创建角色。

  3. 在创建角色页面,按下表选择关键配置,输入角色名称,单击确定。具体操作请参见创建可信实体为阿里云服务的RAM角色。

配置项

建议值

可信主体类型

阿里云账号

信任主体名称

当前云账号,或允许调用STS的账号

角色名称

例如rdscopilot-console,需与上文"为RAM用户授权AssumeRole"步骤中权限策略的<ram_role_name>保持一致

2. 配置RAM角色信任策略

推荐将RAM角色信任策略收敛到上一步创建的后端专用RAM用户,操作步骤如下:

  1. 在角色列表中,单击刚创建的RAM角色名称。

  2. 在角色详情页面,单击信任策略页签,然后单击编辑信任策略。

  3. 选择脚本编辑页签,将策略内容修改为以下示例:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "RAM": "acs:ram::<account_id>:user/<ram_user_name>"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

替换规则:

  • <account_id>:RAM用户所在阿里云主账号ID。

  • <ram_user_name>:上一步创建的RAM用户名称。

检查信任策略时,请重点确认以下内容:

检查项

正确结果

Action

sts:AssumeRole

Principal

包含RAM

Principal.RAM

指向后端专用RAM用户ARN

如果AssumeRole报AccessDenied,请优先检查以下两项:

  1. RAM用户是否有sts:AssumeRole到目标RoleArn的权限。

  2. RAM角色信任策略是否允许该RAM用户扮演。

3. 为RAM角色授权RDS和RDS AI助手权限

为创建的RAM角色授予访问RDS和RDS AI助手的权限,操作步骤如下:

  1. 在角色详情页面,单击权限管理页签,然后单击新增授权。

  2. 在新增授权面板,资源范围选择账号级别。

  3. 在权限策略区域,搜索并勾选目标权限策略,单击确认新增授权。具体操作请参见管理RAM角色的权限。根据业务需求,选择添加以下权限策略:

  • RDS基础权限:

    • AliyunRDSFullAccess:RDS的完整管理权限。

    • AliyunRDSReadOnlyAccess:RDS的只读访问权限。

  • RDS AI助手权限:

    • AliyunRDSAIFullAccess:RDS AI助手的完整管理权限。

    • AliyunRDSCopilotReadOnlyAccess:RDS AI助手的只读访问权限。

重要

为了确保能正常访问RDS AI助手功能,建议至少添加AliyunRDSReadOnlyAccess(RDS基础只读权限)和AliyunRDSCopilotReadOnlyAccess(RDS AI助手只读权限)。如果用户需要在页面内执行管理类操作,可根据业务需要替换为AliyunRDSFullAccess和AliyunRDSAIFullAccess;如果只需要查看实例、咨询、诊断和只读分析能力,建议优先使用只读策略。

4. 获取RAM角色ARN

  1. 使用RAM管理员登录RAM控制台。

  2. 在左侧导航栏,选择身份管理 > 角色。

  3. 在角色页面,单击目标RAM角色名称。

  4. 在RAM角色详情页面的基本信息区域,复制ARN信息。

格式示例:

acs:ram::13427***:role/rdscopilot-console

该ARN需要和RAM用户的sts:AssumeRole授权策略中的Resource保持一致。

配置开发环境

安装依赖

pip install Flask==2.3.0 requests==2.31.0 aliyun-python-sdk-core==2.13.36 aliyun-python-sdk-sts==3.1.0

配置环境变量

export ALIYUN_ACCOUNT_ID="your-account-id" #阿里云账号ID
export ALIYUN_ACCESS_KEY_ID="your-access-key-id" #RAM用户的AccessKeyId
export ALIYUN_ACCESS_KEY_SECRET="your-access-key-secret" #RAM用户的AccessKeySecret
export ALIYUN_RAM_ROLE="your-ram-role-name" #RAM角色名称

完整示例代码与核心逻辑分析

完整示例代码

以下提供一个基于Python Flask框架的完整后端服务和前端页面示例。

后端服务 (app.py)

import os
import json
import requests
from flask import Flask, render_template, jsonify
from aliyunsdkcore.client import AcsClient
from aliyunsdksts.request.v20150401.AssumeRoleRequest import AssumeRoleRequest

app = Flask(__name__, template_folder='.')

# 配置信息(建议使用环境变量)
ACCOUNT_ID = os.environ.get("ALIYUN_ACCOUNT_ID")
ACCESS_KEY_ID = os.environ.get("ALIYUN_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIYUN_ACCESS_KEY_SECRET")
RAM_ROLE = os.environ.get("ALIYUN_RAM_ROLE", "rdscopilot-console")
REGION_ID = "cn-hangzhou"
SIGN_IN_DOMAIN = "https://signin.aliyun.com/federation"
DESTINATION = "https://rdsnext4service.console.aliyun.com/rdsCopilotFree/cn-hangzhou?hideTopbar=true&copilotReference\_min"


def assume_role():
    """通过 STS 获取临时凭证"""
    client = AcsClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET, REGION_ID)
    request = AssumeRoleRequest()
    request.set_RoleArn(f"acs:ram::{ACCOUNT_ID}:role/{RAM_ROLE}")
    request.set_RoleSessionName("rds-ai-assistant-session")
    request.set_DurationSeconds(3600)
    
    response = client.do_action_with_exception(request)
    return json.loads(response)["Credentials"]


def get_signin_token(ak, sk, token):
    """使用临时凭证获取登录 Token"""
    params = {
        "Action": "GetSigninToken",
        "AccessKeyId": ak,
        "AccessKeySecret": sk,
        "SecurityToken": token,
        "TicketType": "mini"
    }
    resp = requests.get(SIGN_IN_DOMAIN, params=params)
    if resp.status_code == 200:
        return resp.json().get("SigninToken")
    else:
        raise Exception(f"Failed to get SigninToken: {resp.status_code}, {resp.text}")


def get_login_url(signin_token):
    """构造最终的登录跳转 URL"""
    params = {
        "Action": "Login",
        "LoginUrl": "https://signin.aliyun.com/login.htm",
        "Destination": DESTINATION,
        "SigninToken": signin_token
    }
    req = requests.Request('GET', SIGN_IN_DOMAIN, params=params).prepare()
    return req.url


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/get_console_url')
def get_url():
    try:
        # Step 1: Assume Role
        creds = assume_role()
        
        # Step 2: Get Signin Token
        signin_token = get_signin_token(
            creds['AccessKeyId'], 
            creds['AccessKeySecret'], 
            creds['SecurityToken']
        )
        
        # Step 3: Construct Login URL
        login_url = get_login_url(signin_token)
        
        return jsonify({"url": login_url})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5023, debug=True)

前端页面(index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RDS AI助手 - 免登录访问</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            height: 100vh;
            box-sizing: border-box;
        }
        .header {
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            gap: 15px;
        }
        button {
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background-color: #0056b3;
        }
        button:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }
        .iframe-container {
            flex: 1;
            border: 1px solid #ddd;
            border-radius: 8px;
            overflow: hidden;
            background-color: #f8f9fa;
            position: relative;
        }
        iframe {
            width: 100%;
            height: 100%;
            border: none;
        }
        .placeholder {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #666;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>RDS AI助手</h1>
        <button id="jumpNewTabBtn">新标签页打开</button>
        <button id="embedIframeBtn">Iframe 嵌入</button>
        <span id="status"></span>
    </div>

    <div class="iframe-container" id="container">
        <div class="placeholder" id="placeholder">
            点击上方按钮,选择加载方式
        </div>
    </div>

    <script>
        const jumpNewTabBtn = document.getElementById('jumpNewTabBtn');
        const embedIframeBtn = document.getElementById('embedIframeBtn');
        const container = document.getElementById('container');
        const placeholder = document.getElementById('placeholder');
        const status = document.getElementById('status');

        async function getConsoleUrl() {
            jumpNewTabBtn.disabled = true;
            embedIframeBtn.disabled = true;
            status.innerText = '正在生成免登录链接...';
            placeholder.innerText = '正在获取免登录链接...';

            try {
                const response = await fetch('/get_console_url');
                const data = await response.json();
                
                if (data.error) {
                    throw new Error(data.error);
                }
                
                return data.url;
            } catch (error) {
                alert('请求异常: ' + error.message);
                placeholder.innerText = '加载失败,请重试';
                return null;
            } finally {
                jumpNewTabBtn.disabled = false;
                embedIframeBtn.disabled = false;
                status.innerText = '';
            }
        }

        jumpNewTabBtn.addEventListener('click', async () => {
            const url = await getConsoleUrl();
            if (url) {
                window.open(url, '_blank');
                placeholder.innerText = '已在新标签页中打开';
            }
        });

        embedIframeBtn.addEventListener('click', async () => {
            const url = await getConsoleUrl();
            if (url) {
                container.innerHTML = `<iframe src="${url}" allow="clipboard-read; clipboard-write"></iframe>`;
            }
        });
    </script>
</body>
</html>

核心逻辑分析

获取临时AccessKey和Token

在您的Web应用后端服务中,使用步骤一中RAM用户的AccessKey,调用STS的AssumeRole接口,获取用于访问云服务的临时身份凭证。

以下为Python SDK示例代码:

import json
from aliyunsdkcore.client import AcsClient
from aliyunsdksts.request.v20150401.AssumeRoleRequest import AssumeRoleRequest

# 配置信息
ACCOUNT_ID = os.environ.get("ALIYUN_ACCOUNT_ID")
ACCESS_KEY_ID = os.environ.get("ALIYUN_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIYUN_ACCESS_KEY_SECRET")
RAM_ROLE = os.environ.get("ALIYUN_RAM_ROLE", "rdscopilot-console")
REGION_ID = "cn-hangzhou"

def assume_role():
    """通过 STS 获取临时凭证"""
    client = AcsClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET, REGION_ID)
    request = AssumeRoleRequest()
    request.set_RoleArn(f"acs:ram::{ACCOUNT_ID}:role/{RAM_ROLE}")
    request.set_RoleSessionName("rds-ai-assistant-session")
    request.set_DurationSeconds(3600)  # Token有效期1小时
    
    response = client.do_action_with_exception(request)
    return json.loads(response)["Credentials"]

返回示例:

{
  "AccessKeyId": "STS.xxx",
  "AccessKeySecret": "xxx",
  "SecurityToken": "CAISxxx...",
  "Expiration": "2026-01-22T10:00:00Z"
}

获取登录Token

通过上一步STS的AssumeRole接口获取临时AccessKey和Token后,调用登录服务接口获取登录Token。

请求登录服务接口示例:

https://signin.aliyun.com/federation?Action=GetSigninToken
&AccessKeyId=<STS返回的临时AccessKeyId>
&AccessKeySecret=<STS返回的临时AccessKeySecret>
&SecurityToken=<STS返回的安全Token>
&TicketType=mini
重要
  • STS返回的SecurityToken可能包含+、/等特殊字符,在作为URL参数传递前,必须进行URL编码。

  • 若您希望将页面嵌入到<iframe>中,请求时必须添加&TicketType=mini参数。

Python示例代码:

import requests
from urllib.parse import quote

SIGN_IN_DOMAIN = "https://signin.aliyun.com/federation"

def get_signin_token(access_key_id, access_key_secret, security_token):
    """使用临时凭证获取登录 Token"""
    params = {
        "Action": "GetSigninToken",
        "AccessKeyId": access_key_id,
        "AccessKeySecret": access_key_secret,
        "SecurityToken": security_token,
        "TicketType": "mini"
    }
    resp = requests.get(SIGN_IN_DOMAIN, params=params)
    if resp.status_code == 200:
        return resp.json().get("SigninToken")
    else:
        raise Exception(f"Failed to get SigninToken: {resp.status_code}, {resp.text}")

返回示例:

{
  "SigninToken": "xxx-xxx-xxx"
}

生成免登录链接

使用上一步获取的SigninToken和目标RDS AI助手页面的URL,构造最终的免登录链接。

说明

由于SigninToken有效期为3小时,建议在自建Web应用中将链接设置为每次请求时生成新登录Token。

  1. 获取RDS AI助手控制台页面URL。

    地域

    域名

    中国站

    rdsnext4service.console.aliyun.com

    URL示例:

    https://rdsnext4service.console.aliyun.com/rdsCopilotFree/cn-hangzhou?hideTopbar=true&copilotReference
  2. 构造免登录访问链接。

    请求格式:

    https://signin.aliyun.com/federation?Action=Login
    &LoginUrl=<登录失效跳转的地址>
    &SigninToken=<获取到的登录SigninToken>
    &Destination=<目标RDS AI助手控制台页面链接>

    Python示例代码:

    import requests
    
    def get_login_url(signin_token, destination_url):
        """构造最终的登录跳转 URL"""
        params = {
            "Action": "Login",
            "LoginUrl": "https://signin.aliyun.com/login.htm",
            "Destination": destination_url,
            "SigninToken": signin_token
        }
        # 使用 requests 构造完整的 URL
        req = requests.Request('GET', SIGN_IN_DOMAIN, params=params).prepare()
        return req.url
  3. 访问免登录链接。
    将生成的最终URL返回给前端。前端可以在<iframe>的src属性中加载此URL,或通过新标签页打开,即可实现免登录访问RDS AI助手。

服务启动

本地环境启动命令

python app.py

生产环境启动命令

生产环境建议使用gunicorn或uWSGI。

gunicorn -w 4 -b 0.0.0.0:5023 app:app

常见问题

  • Q:嵌入的页面一段时间后无法访问或提示登录,是什么原因?
    A:这是因为SigninToken具有时效性(有效期为3小时)。为了获得最佳体验,建议不要在前端缓存生成的免登录URL。应在每次需要加载或刷新嵌入页面时,都由前端实时向后端请求一个新的免登录URL。

    同时,请检查以下内容:

    • GetSigninToken请求是否包含TicketType=mini。

    • 前端是否缓存了旧的federation URL。

    • 后端是否错误返回了已打码或不完整的URL。

  • Q:页面嵌入后提示“权限不足”或无法看到实例列表,如何解决?
    A:可能是RAM角色未配置足够的RDS或RDS AI助手权限。请检查为RAM角色配置的权限。

    1. 确认角色是否已附加AliyunRDSReadOnlyAccess(或AliyunRDSFullAccess)权限,这是查看RDS实例列表的基础。

    2. 确认角色是否已附加AliyunRDSCopilotReadOnlyAccess(或AliyunRDSAIFullAccess)权限,这是使用RDS AI助手功能所必需的。

    3. 如果使用了自定义权限策略,请确保策略中包含访问RDS实例(例如rds:Describe*)和RDS AI助手相关操作的权限。

    说明

    这里应检查RAM角色权限,而不是后端RAM用户权限。后端RAM用户只负责调用AssumeRole。

  • Q:调用AssumeRole报AccessDenied,如何排查?
    A:按以下顺序检查:

    1. RAM用户是否有sts:AssumeRole权限。

    2. RAM用户授权策略中的Resource是否为目标RAM角色的ARN。

    3. RAM角色信任策略中的Principal.RAM是否指向该RAM用户。

    4. 是否误创建了“可信实体为阿里云服务”的角色,导致信任策略中只有Principal.Service。

  • Q:调用GetSigninToken报错或iframe无法加载,如何排查?
    A:按以下顺序检查:

    1. SecurityToken是否作为URL参数正确编码。

    2. 请求GetSigninToken时是否包含TicketType=mini。

    3. 后端是否使用STS返回的临时AK/SK/Token,而不是原始RAM用户AK/SK。

    4. Destination是否整体URL编码,避免内部&被federation URL当成外层参数。

  • Q:RAM用户和RAM角色的权限应该如何分配?
    A:推荐规则如下:

    权限

    授予对象

    sts:AssumeRole

    后端专用RAM用户

    AliyunRDSReadOnlyAccess或AliyunRDSFullAccess

    RDS AI助手访问RAM角色

    AliyunRDSCopilotReadOnlyAccess或AliyunRDSAIFullAccess

    RDS AI助手访问RAM角色

    如果授权方向反了,常见表现是:

    • RAM用户有RDS权限,但AssumeRole失败。

    • RAM角色能被扮演,但页面打开后看不到实例或提示权限不足。