前言
DataWorks 提供丰富的数据可视化界面,让用户能轻松地透过界面操作大数据业务,但仍有集成至自建 Web 界面的需求,减少切换页面的频率。DataWorks 新推出的 Embed API 透过阿里云令牌服务结合自建 Web 界面代理登录阿里云,做到嵌入DataWorks数据地图的血缘图。
Embed API https://dataworks.data.aliyun.com/cn-shanghai/open/playground/case?id=dataLineageGraph
先看效果图
登录态处理
登录态的处理有五个方面要处理
- RAM 帐号
- 代理 Role
- 获取临时 AK
- 获取登录 Token
- 嵌入页面
其顺序图如下:
RAM 帐号
打开 RAM 访问控制,建立一个 RAM 帐号,并赋予 AliyunSTSAssumeRoleAccess 权限 (使此 RAM 帐号有代理 Role 的权限),以及取得 RAM 帐号的 AK,我们将在自建 Web 里使用这个 RAM 帐号登录获取临时 AK。
若使用 Open API 来新建可参考以下页面:
安全提示: 建议在自建 Web 里,其自运维的用户帐号一对一对应到一个阿里云的 RAM 帐号或 Role,例如自建的 LDAP 系统,每新建一个帐号就自动绑定一个阿里云 RAM 帐号或 Role,并且是一对一的关系,不使用共享一个帐号或 Role 的方式。
代理 Role
取得 RAM 帐号的 AK 后,使用这个 RAM 来代理一个 Role 并取得临时 AK,打开角色页面,建立一个角色,并取得这个角色的 ARN 码。
若使用 Open API 来新建可参考以下页面:
获取临时 AK
取得角色 ARN 码后,我们已俱备以下信息:
- RAM 帐号 AK (AccessKeyId, AccessKeySecret)
- Role ARN
- 要登录的 DataWorks 的地域 (如上海为 cn-shanghai)
使用 STS 获取临时 AK 的 Open API 接口,接口描述查看。
透过此接口我们可取到以下信息:
- 临时 AK (AccessKeyId, AccessKeySecret)
- securityToken (安全令牌)
获取登录 Token
取得临时 AK 与安全令牌后,再透过阿里云登录服务 ( signin.aliyun.com/federation ),取得 SigninToken (登录令牌),如以下代码示例。
// get aliyun signing token const signingUrl = 'https://signin.aliyun.com/federation'; const params = new URLSearchParams(); params.append('Action', 'GetSigninToken'); params.append('AccessKeyId', accessKeyId); // 临时 accessKeyId params.append('AccessKeySecret', accessKeySecret); // 临时 accessKeySecret params.append('SecurityToken', securityToken); // 安全令牌 params.append('TicketType', 'mini'); const signingResult = await fetch(signingUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params, }); const result = await signingResult?.json?.(); console.log(result?.SigninToken);
嵌入页面
取得登录令牌后,组织以下路径,透过 iframe 嵌入即可使用 DataWorks 血缘图。
https://signin.aliyun.com/federation?Action=Login&LoginUrl=${encodedLoginUrl}&Destination=${encodedUrl}&SigninToken=${encodedSinginToken}
参数说明:
- encodedLoginUrl: 为登录态失效后的转登页面,通常为 https://signin.aliyun.com
- encodedUrl: 为最终使用 DataWorks 的界面链接,此处需要 DataWorks 提供可嵌入使用的功能链接。
- 血缘图目前透出的使用链接为 https://dataworks4service.data.aliyun.com/地域/dmc/data-lineage?entityGuid=表Guid&entityType=表类型&hideTopbar=true
- encodedSinginToken: 登录令牌
参考代码
代码范例 (以Node.js为例)
- index.js
const express = require('express'); const path = require('path'); const LoginAliyun = require('./loginAliyun'); const fetch = require('node-fetch'); const fs = require('fs'); const http = require('http'); const https = require('https'); const privateKey = fs.readFileSync('./sslcert/example.com.key', 'utf8'); const certificate = fs.readFileSync('./sslcert/example.com.crt', 'utf8'); const credentials = { key: privateKey, cert: certificate }; const app = express(); // Have Node serve the files for our built React app app.use(express.static(path.resolve(__dirname, './client/dist'))); app.get("/getSigninToken", async (req, res) => { const regionId = 'cn-shanghai'; const ramAccessKeyId = 'your ram access key id'; const ramAccessKeySecret = 'your ram access key secret'; const roleArn = 'role arn'; const loginResult = await LoginAliyun.main(ramAccessKeyId, ramAccessKeySecret, regionId, roleArn); const credentials = loginResult?.body?.credentials; if (!credentials) { res.json({ message: 'error' }); return; } // const { arn, assumedRoleUser } = loginResult?.body?.assumedRoleUser || {}; const { accessKeyId, accessKeySecret, securityToken, expiration } = credentials || {}; // get aliyun signing token const signingUrl = 'https://signin.aliyun.com/federation'; const params = new URLSearchParams(); params.append('Action', 'GetSigninToken'); params.append('AccessKeyId', accessKeyId); params.append('AccessKeySecret', accessKeySecret); params.append('SecurityToken', securityToken); params.append('TicketType', 'mini'); const signingResult = await fetch(signingUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params, }); const result = await signingResult?.json?.(); res.json({ data: result?.SigninToken || '' }); }); // All other GET requests not handled before will return our React app app.get('*', (req, res) => { res.sendFile(path.resolve(__dirname, './client/dist', 'index.html')); }); const httpServer = http.createServer(app); const httpsServer = https.createServer(credentials, app); httpServer.listen(8080); httpsServer.listen(8443); console.log('express running at http://localhost:%d', 8080);
- loginAliyun.js
'use strict'; // This file is auto-generated, don't edit it // 依赖的模块可通过下载工程中的模块依赖文件或右上角的获取 SDK 依赖信息查看 const Sts20150401 = require('@alicloud/sts20150401'); const OpenApi = require('@alicloud/openapi-client'); const Util = require('@alicloud/tea-util'); const Tea = require('@alicloud/tea-typescript'); // https://api.aliyun.com/api/Sts/2015-04-01/AssumeRole?lang=NODEJS class Client { /** * 使用AK&SK初始化账号Client * @return Client * @throws Exception */ static createClient(accessKeyId, accessKeySecret, regionId) { // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。 // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html。 let config = new OpenApi.Config({ // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。 accessKeyId: process.env['ALIBABA_CLOUD_ACCESS_KEY_ID'] || accessKeyId, // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 accessKeySecret: process.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET'] || accessKeySecret, }); // Endpoint 请参考 https://api.aliyun.com/product/Sts config.endpoint = `sts.${regionId}.aliyuncs.com`; return new Sts20150401.default(config); } static async main(accessKeyId, accessKeySecret, regionId, roleArn, roleSessionName, durationSeconds) { let result; let client = Client.createClient(accessKeyId, accessKeySecret, regionId); let assumeRoleRequest = new Sts20150401.AssumeRoleRequest({ roleArn: roleArn, roleSessionName: roleSessionName || 'ram', durationSeconds: durationSeconds || 3600, // 有效期,单位为秒 }); let runtime = new Util.RuntimeOptions({}); try { // 复制代码运行请自行打印 API 的返回值 result = await client.assumeRoleWithOptions(assumeRoleRequest, runtime); } catch (error) { // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 // 错误 message console.log(error.message); // 诊断地址 console.log(error.data["Recommend"]); Util.default.assertAsString(error.message); } return result; } } module.exports = Client;
- package.json
{ "name": "aliyun-iframe-dataworks-server", "version": "1.0.0", "description": "Aliyun Iframe DataWorks", "private": true, "main": "/pages/index.html", "scripts": { "start": "node index.js" }, "dependencies": { "@alicloud/openapi-client": "0.4.7", "@alicloud/sts20150401": "1.1.3", "@alicloud/tea-typescript": "1.8.0", "@alicloud/tea-util": "1.4.7", "express": "4.17.1", "node-fetch": "2.7.0", "path": "^0.12.7" }, "engines": { "node": ">=16.16.0" } }
- client/dist/index.html
<head> <style> #url-input { width: 600px; } iframe { margin: 16px; width: calc(100% - 32px); height: 680px; } </style> <script> window.fetchSigninToken = async () => { let result = ''; try { const response = await fetch('/getSigninToken'); const json = await response?.json?.(); result = json?.data; } catch (e) { console.error(e); } return result; }; window.onUrlChange = async () => { try { const url = document.getElementById('url-input').value; if (!url) return; const signinToken = await fetchSigninToken(); if (!signinToken) return; const encodedLoginUrl = encodeURIComponent('https://signin.aliyun.com'); const encodedUrl = encodeURIComponent(url); const encodedSinginToken = encodeURIComponent(signinToken); const iframe = document.getElementById('iframe-web'); iframe.src = `https://signin.aliyun.com/federation?Action=Login&LoginUrl=${encodedLoginUrl}&Destination=${encodedUrl}&SigninToken=${encodedSinginToken}`; } catch (e) { console.error(e); } }; </script> </head> <body> <div id="message"></div> 嵌入的页面: <span>URL: </span><input id="url-input" type="text" /> <button onclick="onUrlChange();">刷新</button> <div> <iframe id="iframe-web" frameBorder="0"></iframe> </div> </body>
- sslcert 文件夹下新建 example.com.crt 与 example.com.key
- 参考新建方式
修改 index.js 以下部份:
const regionId = 'cn-shanghai'; // 要登录 DataWorks 的地域 const ramAccessKeyId = 'your ram access key id'; // RAM 帐户的 Access Key Id const ramAccessKeySecret = 'your ram access key secret'; // RAM 帐户的 Access Key Secret const roleArn = 'role arn'; // 角色 ARN
执行以下指令:
yarn install # 安装依赖 yarn start
打开路径 https://localhost:8443/,并填入要嵌入的页面链接
小结与后续
STS 令牌服务登录业务是阿里云提供的嵌入方式,其它业务如 OpenTelemetry、嵌入管控台也都是使用此方法,但使用此方法后,仍有一些问题需要解决:
- 被嵌入的业务方,需提供可嵌入的功能模块、链接格式、可提供的参数透出给接入方
- 被嵌入的业务模块,需要隐藏如公共头、侧边栏以及不必要的跳转,也需要被嵌入的业务方来调整
- 当使用 RAM 帐号透过 STS 令牌服务登录 DataWorks 后,若单独打开 DataWorks 链接也会是登录状态,其原因是因为 STS 令牌服务的登录失效时间与 DataWorks 登录失效时间不一致导致,后续若统一调整为阿里云登录失效时间后,此问题可解
- 虽然我们只给 RAM 帐号 AliyunSTSAssumeRoleAccess 的权限,但是仍需要接入方在自建的 Web 系统上,将自营的帐号一对一对应到 RAM 帐号或 Role,让每个登录到自建的 Web 的用户,都是独立使用一个 RAM 帐号或 Role (不使用共享一个帐号或 Role 的方式),提高安全性
- 确保每次都能完整初始化 DataWorks 页面,可在背景 (隐藏 iframe) 先嵌入完整页面如 https://dataworks4service.data.aliyun.com/cn-shanghai/dmc,再将 iframe 链接调整至功能模块 (血缘图)