将外呼机器人的配置场景通过Iframe方式集成到用户自己的系统上使用。
参考阿里云免登方案。
一、前置条件
创建RAM子用户及授权,如子账号已存在,可跳过该步骤
1、RAM 用户
1.1、添加授权
调用 AssumeRole 的身份,需具备 AliyunSTSAssumeRoleAccess 权限

1.2、凭据配置(AK/SK)
创建AccessKey,然后通过环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET 提供给后端服务。

2、创建RAM角色
2.1、创建RAM角色,或者通过RAM的API CreateRole创建RAM角色。



2.2、为刚刚创建的角色授权,AliyunOutboundbotFullAccess
用于访问智能外呼产品,按需授予 AliyunOutboundBotFullAccess


2.3、角色 ARN 参数获取,注意是角色不是用户的

二、Java代码示例
1、Maven 依赖:
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.9</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
<version>0.2.23</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>openapiutil</artifactId>
<version>0.2.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>credentials-java</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>2、Code Sample
流程:AssumeRole 拿临时凭据 → GetSigninToken 换登录令牌 → 把令牌返回给前端。
服务端到此为止,不再关心要嵌入哪个页面。Destination 与最终免登链接由前端拼装,见四、前端集成(HTML+JS)。
配置环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET
package com.aliyun.sample;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class IframeLoginSample {
/** STS 服务接入地址,参考 https://api.aliyun.com/product/Sts */
private static final String STS_ENDPOINT = "sts.cn-hangzhou.aliyuncs.com";
/** 阿里云联合登录(免登录)网关,服务端只用它换取 SigninToken */
private static final String SIGN_IN_URL = "https://signin.aliyun.com/federation";
// 改为你自己的 roleArn,格式 acs:ram::<主账号ID>:role/<角色名>
private static final String ROLE_ARN = "acs:ram::xxxxxxxxxxxxxxxx:role/aliyunoutboundbot4serviceaccessingrole";
private static final Gson GSON = new Gson();
private static class Credentials {
private String accessKeyId;
private String accessKeySecret;
private String securityToken;
}
/**
* <b>description</b> :
* <p>使用凭据初始化账号Client</p>
*
* @return Client
*
* @throws Exception
*/
public static com.aliyun.teaopenapi.Client createClient() throws Exception {
// 运行前需配置环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET,凭据链会自动读取。
// 该 AK 所属的 RAM 用户需具备 AliyunSTSAssumeRoleAccess 权限。
// 工程代码建议使用更安全的无AK方式,凭据配置方式请参见:https://help.aliyun.com/document_detail/378657.html。
com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setCredential(credential);
// Endpoint 请参考 https://api.aliyun.com/product/Sts
config.endpoint = STS_ENDPOINT;
return new com.aliyun.teaopenapi.Client(config);
}
/**
* <b>description</b> :
* <p>API 相关</p>
*
* @return OpenApi.Params
*/
public static com.aliyun.teaopenapi.models.Params createApiInfo() throws Exception {
com.aliyun.teaopenapi.models.Params params = new com.aliyun.teaopenapi.models.Params()
// 接口名称
.setAction("AssumeRole")
// 接口版本
.setVersion("2015-04-01")
// 接口协议
.setProtocol("HTTPS")
// 接口 HTTP 方法
.setMethod("POST")
.setAuthType("AK")
.setStyle("RPC")
// 接口 PATH
.setPathname("/")
// 接口请求体内容格式
.setReqBodyType("json")
// 接口响应体内容格式
.setBodyType("json");
return params;
}
/**
* <b>description</b> :
* <p>通过 AssumeRole 获取临时 AK / SK / SecurityToken</p>
* <p>参考 https://help.aliyun.com/document_detail/28763.html</p>
*/
@SuppressWarnings("unchecked")
private static Credentials assumeRole(com.aliyun.teaopenapi.Client client) throws Exception {
com.aliyun.teaopenapi.models.Params params = createApiInfo();
// runtime options
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
Map<String, Object> query = new LinkedHashMap<>();
query.put("RoleArn", ROLE_ARN);
// 用于区分不同令牌,可用于访问审计。格式:^[a-zA-Z0-9\.@\-_]+$
query.put("RoleSessionName", "outboundbot-iframe-" + System.currentTimeMillis());
// 过期时间,单位秒,取值范围 900 ~ 3600
query.put("DurationSeconds", 3600);
com.aliyun.teaopenapi.models.OpenApiRequest request = new com.aliyun.teaopenapi.models.OpenApiRequest()
.setQuery(com.aliyun.openapiutil.Client.query(query));
// 返回值实际为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
Map<String, ?> resp = client.callApi(params, request, runtime);
Map<String, ?> body = (Map<String, ?>) resp.get("body");
Map<String, ?> raw = body == null ? null : (Map<String, ?>) body.get("Credentials");
if (raw == null) {
throw new IllegalStateException("AssumeRole 响应缺少 Credentials");
}
Credentials credentials = new Credentials();
credentials.accessKeyId = (String) raw.get("AccessKeyId");
credentials.accessKeySecret = (String) raw.get("AccessKeySecret");
credentials.securityToken = (String) raw.get("SecurityToken");
return credentials;
}
/**
* <b>description</b> :
* <p>使用安全令牌获取登录令牌</p>
* <p>参考 https://help.aliyun.com/document_detail/91913.html</p>
* <p>TicketType 固定为 mini:默认票 login_aliyunid_ticket 是 SameSite=Lax,
* 跨站 iframe 不会携带;mini_login_aliyunid_ticket 是 SameSite=None,故嵌入场景必须用 mini 票。</p>
*/
private static String getSigninToken(Credentials credentials) throws Exception {
Map<String, String> query = new LinkedHashMap<>();
query.put("Action", "GetSigninToken");
query.put("AccessKeyId", credentials.accessKeyId);
query.put("AccessKeySecret", credentials.accessKeySecret);
query.put("SecurityToken", credentials.securityToken);
query.put("TicketType", "mini");
HttpURLConnection connection = (HttpURLConnection) URI
.create(SIGN_IN_URL + "?" + encodeQuery(query))
.toURL()
.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.setInstanceFollowRedirects(false);
connection.setRequestProperty("Cache-Control", "no-store");
int statusCode = connection.getResponseCode();
if (statusCode != 200) {
connection.disconnect();
throw new IllegalStateException("GetSigninToken 请求失败(HTTP " + statusCode + ")");
}
String payload = readAll(connection.getInputStream());
connection.disconnect();
Map<String, String> result = GSON.fromJson(payload, new TypeToken<Map<String, String>>() {
}.getType());
String signinToken = result == null ? null : result.get("SigninToken");
if (signinToken == null || signinToken.isEmpty()) {
throw new IllegalStateException("GetSigninToken 响应缺少 SigninToken");
}
return signinToken;
}
private static String encodeQuery(Map<String, String> query) throws Exception {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : query.entrySet()) {
if (builder.length() > 0) {
builder.append('&');
}
builder.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append('=')
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
return builder.toString();
}
private static String readAll(InputStream stream) throws Exception {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
public static void main(String[] args) throws Exception {
com.aliyun.teaopenapi.Client client = createClient();
// Step 1 通过 AssumeRole 获取临时 AK / SK / SecurityToken
Credentials credentials = assumeRole(client);
// Step 2 用临时凭据换取 SigninToken
String signinToken = getSigninToken(credentials);
// Step 3 输出 SigninToken。Destination 与免登链接由前端拼装,见文档第 4 节。
// 实际集成时把上面三步包进你的 Web 接口,返回同样的 JSON,并且该接口必须鉴权。
// SigninToken 等同登录凭据:不要写入日志、不要提交到代码库。
Map<String, String> result = new LinkedHashMap<>();
result.put("signinToken", signinToken);
System.out.println(GSON.toJson(result));
}
}三、页面路径对照表
基准地址:https://outboundbot4service.console.aliyun.com/#
页面 | 路径 |
大模型场景列表(提示词模式) |
|
大模型场景详情(提示词模式) |
|
大模型场景列表(工作流程编排) |
|
大模型场景详情(工作流程编排) |
|
大模型场景列表(函数计算模式) |
|
大模型场景详情(函数计算模式) |
|
大模型声音克隆 |
|
大模型全局配置 |
|
大模型问答管理 |
|
知识管理 |
|
小模型场景管理 |
|
小模型场景详情 |
|
对话工厂权限 |
|
四、前端集成(HTML+JS)
服务端只返回 SigninToken,Destination 和最终免登链接由前端拼装。这样要嵌哪个页面、用哪个实例和场景,
都由前端按需决定,后端不必为每个页面单独出接口。
iframe 属性要求
外呼涉及语音交互,嵌入时必须开放以下两个属性:
allow="microphone *" allowfullscreen完整示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>智能外呼控制台嵌入示例</title>
<style>
html, body { margin: 0; height: 100%; }
#console-container { height: 100vh; }
#console-container iframe { display: block; width: 100%; height: 100%; border: 0; }
</style>
</head>
<body>
<div id="console-container"></div>
<script>
(function () {
// ===== 配置区 =====
var SIGN_IN_URL = 'https://signin.aliyun.com/federation';
var CONSOLE_URL = 'https://outboundbot4service.console.aliyun.com/';
// 后端接口获取 signinToken 由第二节 Java 逻辑提供服务。
var TOKEN_API = '/api/outboundbot-signin-token';
// 改为你自己的业务空间(实例)ID
var INSTANCE_ID = '请替换为实例ID';
// 改为你自己的场景 ID,仅「场景详情」类页面需要
var SCRIPT_ID = '请替换为场景ID';
// 只接受外呼控制台域名发来的消息
var ALLOWED_ORIGINS = ['https://outboundbot4service.console.aliyun.com'];
// 防抖窗口:避免控制台连续上报失效导致无限重载
var RELOAD_COOLDOWN = 3000;
// 页面路径清单,均为「不带实例前缀」的基准路径,详见第 3 节
var PAGES = {
promptText: '/outboundbot_prompt_script/text', // 大模型场景列表(提示词模式)
promptWorkflow: '/outboundbot_prompt_script/workflow', // 工作流程编排(含流程管理,用 tab 区分)
promptFunction: '/outboundbot_prompt_script/function', // 函数计算模式
globalVariable: '/outboundbot_prompt_script/global_variable', // 全局配置
voiceClone: '/outboundbot_prompt_script/llm_voice_clone', // 声音克隆
llmFaq: '/outboundbot_prompt_script/llm_faq', // 问答管理
llmDoc: '/outboundbot_prompt_script/llm_doc', // 知识管理
promptDetail: '/outboundbot_prompt_script/text/prompt_script_detail', // 大模型场景详情,text 可换成 workflow / function
script: '/outboundbot_script', // 小模型场景管理
dsScriptDetail: '/outboundbot_script/ds_scriptsdetail' // 小模型场景详情
};
// 要嵌入的页面,按需替换
var PAGE_PATH = PAGES.promptDetail;
// ===== 链接拼装 =====
// 业务空间路由统一以 /:instanceId 为前缀;
// 漏拼前缀会触发一次补全重定向,还可能落到错误的业务空间
function createDestination(pagePath) {
var query = new URLSearchParams({ instanceId: INSTANCE_ID });
// 仅「场景详情」类页面需要 scriptId
if (/\/(prompt_script_detail|ds_scriptsdetail)$/.test(pagePath)) {
query.set('scriptId', SCRIPT_ID);
}
// 工作流程编排页面包含流程管理,用 tab 区分
if (pagePath === PAGES.promptWorkflow) {
query.set('tab', 'flow');
}
return CONSOLE_URL + '#/' + INSTANCE_ID + pagePath + '?' + query.toString();
}
function createLoginUrl(signinToken, destination) {
var query = new URLSearchParams({
Action: 'Login',
// 登录失效后的跳转地址,一般配置为自建服务上会 302 回本页的 URL
LoginUrl: 'https://signin.aliyun.com/login.htm',
Destination: destination,
SigninToken: signinToken
});
return SIGN_IN_URL + '?' + query.toString();
}
// ===== 加载与刷新 =====
var container = document.getElementById('console-container');
var frame = null;
var lastLoadAt = 0;
function fetchSigninToken() {
return fetch(TOKEN_API, { cache: 'no-store', credentials: 'same-origin' })
.then(function (response) {
if (!response.ok) {
throw new Error('获取 SigninToken 失败(HTTP ' + response.status + ')');
}
return response.json();
})
.then(function (result) {
if (!result || !result.signinToken) {
throw new Error('响应缺少 signinToken');
}
return result.signinToken;
});
}
function mountFrame(src) {
if (frame && frame.parentNode) {
frame.parentNode.removeChild(frame);
}
frame = document.createElement('iframe');
frame.src = src;
frame.title = '智能外呼控制台';
frame.setAttribute('allow', 'microphone *');
frame.setAttribute('allowfullscreen', '');
// 避免带 SigninToken 的 URL 通过 Referer 泄漏给第三方
frame.setAttribute('referrerpolicy', 'no-referrer');
container.appendChild(frame);
}
function loadConsole() {
var now = Date.now();
if (now - lastLoadAt < RELOAD_COOLDOWN) {
return;
}
lastLoadAt = now;
fetchSigninToken()
.then(function (signinToken) {
mountFrame(createLoginUrl(signinToken, createDestination(PAGE_PATH)));
})
.catch(function (error) {
// 不要把 error.message 直接展示给终端用户,其中可能含服务端细节
console.error('[outboundbot] 免登加载失败', error);
});
}
function handleMessage(event) {
// 双重校验:来源域名 + 来源窗口。
// 只校验 origin 时,同域名下的其它 iframe 或被劫持的页面也能伪造消息。
if (ALLOWED_ORIGINS.indexOf(event.origin) === -1) {
return;
}
if (!frame || event.source !== frame.contentWindow) {
return;
}
// 登录态失效:{ code: 'ConsoleNeedLogin', message: '登录超时,请重新登录' }
if (event.data && event.data.code === 'ConsoleNeedLogin') {
loadConsole();
}
}
window.addEventListener('message', handleMessage);
loadConsole();
})();
</script>
</body>
</html>父页面与 iframe 都必须走 https。写成 http://signin.aliyun.com/... 在 https 页面下会被浏览器按混合内容拦截。
postMessage 协议
外呼控制台在请求接口发现登录 cookie 失效时,会向父页面发送:
{ "code": "ConsoleNeedLogin", "message": "登录超时,请重新登录" }父页面收到后自行决定刷新 iframe 或跳转登录页。刷新时前端会重新请求 TOKEN_API 换取新令牌,
因此不要把首次拿到的免登链接缓存下来复用——SigninToken 有时效,复用会直接失败。
