全部产品

工作台托管-技术接入指南

对接流程

image.png

架构图

simple.png

调用链路

image.png

说明

ISV无法拿到用户的ak,Aliyun会针对用户的请求颁发accessToken,ISV要调用OPEN API,得携带accessToken调用Aliyun-Api-Service。

前端改造

基础框架

目前没有对使用何种 JS 框架做约束,支持 React / VUE / Angular 三大框架。

但是需要对入口文件初始化代码逻辑稍加改造,从而能接入阿里云的微前端容器。具体如下:

Angular

// 引入一个 portal 包
import { boostrap } from '@alicloud/console-os-ng-portal';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { AppModule } from './app/app.module';

// 修改一下入口写法并 export
export default boostrap({
  bootstrapFunction: props => {
    return platformBrowserDynamic().bootstrapModule(AppModule);
  },
  template: '<app-root />',
  Router,
  NgZone: NgZone,
});

React

// 引入一个 portal 包
import { mount }  from '@alicloud/console-os-react-portal';
import React from 'react';
import App from './app';

// 修改一下入口写法并 export
export default mount(
  (props) => <App {...props} />,
  document.getElementById('app')
);

VUE

// 引入一个 portal 包
import { mount } from '@alicloud/console-os-vue-portal';
import App from './App.vue';

// 修改一下入口写法并 export
export default mount({
  el: '#app',
  i18n,
  router,
  store,
  render: h => h(App),
  created() {}
});

组件库

截止目前,没有要求强制使用阿里云提供的组件库 SDK。

但是在应用的 UI 设计上,需要阿里云的设计师评估。风格差异太大的话则需要修改。

请求调用

由于后端服务会部署在阿里云这边,所以请求的地址和消息体需要修改:

General
- Request URL: https://work.console.aliyun.com/data/plugin.json
- Request Method: POST

Form Data
- product: ${isvCode}-${appCode}
- action: ${actionName}
- params: ${bizParamsMap}
- sec_token: ${window.ALIYUN_CONSOLE_CONFIG.SEC_TOKEN}

返回的数据是:

{
  "requestId": String,
  "code": String,
  "successResponse": Boolean,
  "data": Object
}

工程化

根目录下增加一个 abc.json 文件。此文件用来给阿里云内部构建使用,文件名和路径不可自定:

{
  "assets": {
    "type": "builder",
    "builder": {
      "name": "@ali/builder-breezr"
    }
  }
}

并且增加一个 build-utils.js 文件。此文件提供一个实用函数,用来给构建逻辑调用,文件名和路径可以自定:

const path = require('path');

// ISV 自己托管的资源根目录地址,比如 https://somewhere.oss-cn-hangzhou.aliyuncs.com/dest
const ISV_CDN_PATH = '/';
const CONSOLE_OS_DEV_CDN_PATH = 'http://localhost:4200';

const isCloudBuild = () => {
  return process.env.BUILD_ENV === 'cloud';
};

// 云构建时涉及到的一些逻辑
const getPublicPathFromArg = () => {
  const argvString = process.env.BUILD_ARGV;
  const outputPublicPath = argvString.split('=')[1];
  let publicPath = outputPublicPath.substr(0, outputPublicPath.length - 3);
  if (!publicPath.endsWith('/')) {
    publicPath += '/';
  }
  return publicPath;
};

// 得到最终的资源线上根目录地址
const getPublicPath = () => {
  return process.env.CONSOLE_OS_DEV_LOCAL === 'true' ? 
    CONSOLE_OS_DEV_CDN_PATH : (
      isCloudBuild() ? 
        getPublicPathFromArg() : 
        ISV_CDN_PATH
    );
};

// 构建的目标目录
const getOutputDir = () => {
  return isCloudBuild() ? 
    path.resolve(process.cwd(), process.env.BUILD_DEST_DIR || process.env.BUILD_DEST) : 
    path.resolve(__dirname, 'dist');
};

module.exports = { 
  getPublicPath,
  getOutputDir
};

最后,改造构建逻辑。根据不同框架下的项目修改对应的构建配置文件。

Angular 工程

修改 webpack.config.js,作如下修改:

const osAngularWebpack = require('@alicloud/console-os-ng-builder');
const { getPublicPath, getOutputDir } = require('./build-utils');

module.exports = osAngularWebpack({
  output: {
    // 构建输出目录
    path: getOutputDir(),
    // 部署到的根目录地址,使用 build-utils.js 提供的实用函数
    publicPath: getPublicPath()
  }
}, {
  // 新增应用 ID 作为钩子
  id: `${isvCode}-${appCode}`
});

直接使用 Webpack 构建的工程(React / Non-vue-cli 等)

修改 webpack.config.js,作如下修改:

const Chain = require('webpack-chain');
const merge = require('webpack-merge');
const { chainOsWebpack } = require('@alicloud/console-toolkit-plugin-os');
const { getPublicPath, getOutputDir } = require('./build-utils');

const chain = new Chain();
chainOsWebpack({ 
  // 新增应用 ID 作为钩子
  id: `${isvCode}-${appCode}` 
})(chain);

module.exports = merge({
  output: {
    // 构建输出目录
    path: getOutputDir(),
    // 部署到的根目录地址,使用 build-utils.js 提供的实用函数
    publicPath: getPublicPath()
  }
}, chain.toConfig());

使用了 vue-cli 的工程

修改 vue.config.js,增加几个 option 如下:

const { getPublicPath, getOutputDir } = require('./build-utils');
const path = require('path');

module.exports = {
  // 构建输出目录
  outputDir: getOutputDir(),
  // 部署到的根目录地址,使用 build-utils.js 提供的实用函数
  publicPath: getPublicPath(),
  // 插件配置
  pluginOptions: {
    consoleOs: {
      // 新增应用 ID 作为钩子
      id: `${isvCode}-${appCode}` 
    }
  }
};

并且安装 devDeps:

npm install vue-cli-plugin-console-os -D

部署

ISV 将前端代码仓库托管到 云效,并且增加 console-fe 这个用户作为仓库成员。阿里云这边会执行取代码、运行构建脚本、部署到 CDN 整套逻辑。

最终上线的应用地址是:https://work.console.aliyun.com/${isvCode}-${appCode}

前端资源文件会部署到 https://cb.alibabausercontent.com 这个 CDN 下,而不是应用地址下。

后端服务

后端服务部署在ISV账号申请的vpc里面,此账号为阿里云持有管控。不提供登录,会提供日志查询权限。

ISV的服务要以约定的形式发布,一个action代表一个服务。

  • url :http://${isv_ip}/api/${action}?userInfo=${userInfo}&accessToken=${accessToken}&params=${params}

  • method: get & post

  • 入参:

    • action: ISV自定义的action

    • params: 前端传递过来的参数,toJsonString,URLEncoder.encode(params)

    • userInfo: AliYun OneConsole透传,toJsonString,URLEncoder.encode(userInfo)

      • aliyunPK: String 阿里云账号id

      • parentPk: String 阿里云主账号

      • accountStructure: Integer, 账号类型 2-主账号 3-子账号 4-角色账号

      • roleId: 角色id(角色账号登陆的情况才存在)

      • roleName: 角色name(角色账号登陆的情况才存在)

    • accessToken: Aliyun OneConsole生成,用户权限认证

    • traceId,请将参数传给aliyun-api-service,保证请求可被追踪回溯,排查问题的依据。

      [规范]

    • locale,包括zh_CN、en_US和ja,分别表示中文、英文和日文

  • ISV返回数据的结构

// 正常返回和错误返回都要按这个格式
{
    requestId: String,
  code: String,
  message: String,
  data: <T>
}

ISV 调用 Aliyun-Api-Service

api-service是一个proxy,isv可以通过accessToken和appToken直接调用OPEN API。这里,Aliyun针对每个ISV做了调用白名单,只有在白名单里的产品才能被调用。

    • product: request.getSysProduct(), sdk里的产品ID

    • version: request.getSysVersion(),sdk里的api version

    • params: OPEN API参数,toJsonString,URLEncoder.encode(params),如果是post,不需要encode。

    • accessToken: OneConsole生成,用户权限认证,为了向后兼容,该字段依然存在

    • consoleKey: 调用api-service的凭证,即原来的 accessToken

    • consoleSecret: 调用api-service的凭证,即原来的 accessToken

    • action:对应OPEN API action

    • regionId: 调用的OPEN API region,我们会依据这个拿云产品的endpoint

    • traceId,请将参数传给aliyun-api-service,保证请求可被追踪回溯,排查问题的依据。

      [规范]

  • 出参

正常返回OPEN API的请求结果

错误格式也满足错误OPEN API格式,具体参照「api-service错误码」

离线任务接口

针对离线的任务,也提供给ISV里面accessToken,但需要用户sts授权。

    • uid: 主账号uid

    • appToken:颁发给ISV的唯一身份token,不是isvCode和appCode,请不要泄露。

  • 出参

// data里是accessToken
{
    requestId: String,
  code: String,
  message: String,
  data: String
}

测试阶段,用自身账号做角色扮演测试,测试也请根据权限最小原则来添加策略内容,避免上线前再做回归测试。示例:

{
    "Version": "1",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecs:DescribeRegions",
                "ecs:DescribeAvailableResource",
                "ecs:DescribeImages",
                "ecs:CreateImage"
            ],
            "Resource": [
                "*"
            ],
            "Condition": {}
        },
        {
            "Effect": "Allow",
            "Action": [
                "vpc:DescribeVpcs",
                "vpc:DescribeVSwitches"
            ],
            "Resource": [
                "*"
            ],
            "Condition": {}
        }
    ]
}

api-service 错误码

错误码分为三种:

  • OPEN API的返回结果直接透传,错误码和http状态码参照各个云产品,比如

    Ecs

  • api-service返回的业务错误码,http状态码统一返回400,参照ErrorCode表

  • api-service目前无法感知的异常,错误码用SystemError,http状态码统一返回500,表示内部错误,请联系我们排查问题!

public enum ErrorCode {

    ApiNotExist("ApiNotExist", "The specified api is not exist"),
    ApiVersionNotExist("ApiVersionNotExist", "The specified api version is not exist"),
    ApiActionNotExist("ApiActionNotExist", "The specified api action is not exist"),
    ApiDefineNotExist("ApiDefineNotExist", "The specified restful api define is not exist"),
    ApiDefineWrong("ApiDefineWrong", "The specified restful api define is wrong"),
    UnSupportedApiType("UnSupportedApiType", "The specified api type is not supported"),
    ApiInvokeException("ApiInvokeException", "The api call has some accidents, please try again"),
    ApiFormatException("ApiFormatException", "The specified api information parsing failed"),
    ApiParamsEncodingException("ApiParamEncodingException", "Some parameters of this specified api encoding error"),
    ApiEndpointFormatWrong("ApiEndpointFormatWrong", "The specified api endpoint format error"),
    ApiEndpointNotExist("ApiEndpointNotExist", "The specified api endpoint is not exist"),
    AccountInfoDecodeError("AccountInfoDecodeError", "Account information parsing failed"),
    SystemError("SystemError", "Some accidents have appeared in the system, please try again"),
    AccountInfoNotExist("AccountInfoNotExist", "Account information does not exist"),
    ApiVersionForbidden("ApiVersionForbidden", "The version param is not allowed here"),
    ForbiddenAction("ForbiddenAction", "The specified api action is forbidden"),
    MultiApiContentEmpty("MultiApiContentEmpty", "Content of this request is empty"),
    MultiApiActionMismatch("MultiApiActionMismatch", "The specified api action is not allowed"),
    ApiNoResponse("ApiNoResponse", "This api failed to respond, please try again"),
    ApiReadTimeOut("ApiReadTimeOut", "This api read timed out, please try again"),
    ApiConnectTimeout("ApiConnectTimeout", "This api connect timed out, please try again"),
    ApiConnectionRefused("ApiConnectionRefused", "This api connection refused, please try again"),
    ApiConnectionReset("ApiConnectionReset", "This api connection reset, please try again"),
    ApiUnknownEndpoint("ApiUnknownEndpoint", "This api endpoint is not reachable"),
    ApiEndpontNoRouteToHost("ApiEndpontNoRouteToHost", "This api endpoint is not reachable"),
    MissingApiParam("MissingApiParam", "Missing the necessary request parameters, please add and try again"),
    StsTokenError("StsTokenError", "get sts token failed"),
    InvalidParams("InvalidParams", "The params cannot convert to Map"),
    AppTokenNotExist("AppTokenNotExist", "isv app token not exist"),
    AccessTokenInvalid("AccessTokenInvalid", "access token invalid"),
    ActionNotInWhiteList("ActionNotInWhiteList", "this action is not in white list");

    private String code;
    private String msg;
}

用户购买流程回调

用户通过阿里云市场购买插件服务,购买完成我们会回调isv的接口告知用户的购买状态,包括购买,过期,续费等。

ISV提供的回调服务格式同「 ISV后端服务 」

具体的action含义参考 文档

OSS使用

需要使用oss的场景,为了防止用户数据泄露,我们不允许ISV使用自己账号的OSS。通过sts的方式,isv能获取到指定bucket的操作ak。

    • appToken: String.同离线调用,需要提前找我们做白名单处理。

  • 出参

// data里是sts ak信息
{
    requestId: String,
  code: String,
  message: String,
  data: {
    "SecurityToken":"xxx",
    "AccessKeyId":"xxxx",
    "AccessKeySecret":"xxx",
    "Expiration":"2020-06-17T08:11:31Z"
  }
}

测试环境

出于安全考虑,我们需要对各个环节都做白名单处理,测试环境架构图:

image.png

isv可以本地调试前端代码,并把后端代码放在自己的环境(有公网IP)下进行调试。

需要isv提供的配置如下:

{
    "[isvCode]-[appCode]":{ // isvCode标识isv名,appCode表示插件名,由isv提供。
       "local_debug": {
            "is_debug": true, // 是否开始本地调试【isv填写】
            "url":"http://218.17.169.171:31574" // 后端服务的公网ip&port【isv填写】
        },
        "online":{
            "ips":[
                "xxxxxxxx", // 访问前端的来源ip白名单【isv提供】
            ],
            "account":{ // 测试账号的uid、ak 【isv提供】
                "uid": xxxxxx,
                "accessKeyId":"xxxxx",
                "accessKeySecret":"xxxxxx"
            }
        },
        "offline":{
            "ips":[ // isv后端服务公网ip 【isv填写】
                "xxxxxx"
            ],
            "appToken":"xxxxx",  //由阿里颁发
            "uids":[ // 允许sts调用的uid 【isv填写】
                xxxxxx
            ]
        }
    }
}

说明

  • 本机的公网IP(

    http://www.cip.cc/

    )发给我们,做白名单处理,否则无权限访问。

  • isv需按照上述json结构提供数据,如无sts调用需求,则offline部分无需提供。

  • isvCode标识isv名,appCode表示插件名,由isv提供。

  • 支持sts调用,isv需要在允许sts调用的uid所在账号创建一个ram role,命名参考图中,并给console.aliyuncs.com授权,阿里这边需要在api-service上配置role name。如图:

    image.png
  • 需要绑定host:114.55.202.134 work.console.aliyun.test

  • 测试环境,api-service的访问通过114.55.202.134 访问即可

  • 前端页面访问路径

    http://work.console.aliyun.test/[isvCode]-[appCode]

Mock接口

背景

我们假定拥有以上元数据

  1. isvCode: testIsv

  2. appCode: testApp

  3. 一个RAM用户(子用户)登录阿里云打开使用插件 子用户的UID 为 12345678 对应的主账号的UID为88888888

  4. 调用一个ISV封装的 testAction的接口

  5. 阿里云给ISV提供的服务地址为11.22.33.44

因为阿里云要求阿里云必须在/data 这个路由下提供接口, 即真实的调用路径为 http://11.22.33.44/data/testAction

  1. ISV通过这个接口需要的参数为pageSize 和 pageNumber

前端调用

General
- Request URL: https://work.console.aliyun.com/data/plugin.json
- Request Method: POST

Form Data
- product: testIsv-TestApp
- action: testAction
- params: {"pageSize": 1, pageNumber: "20"} //这些是ISV自己定义的需要透传的参数
- sec_token: XjJHIHy0HDf1I2bLotfLGE //从 window.ALIYUN_CONSOLE_CONFIG.SEC_TOKEN获取

经过阿里云预处理后传递给ISV的参数

http://11.22.33.44/api/testAction?userInfo=%7B%22aliyunPk%22%3A%2212345678%22%2C%22parentPk%22%3A%228888888%22%2C%22accountStructure%22%3A3%2C%22roleId%22%3A%22%22%2C%22roleName%22%3A%22%22%7D}&accessToken=${accessToken&params=%7B%22pageSize%22%3A1%2C%22pageNumber%22%3A%2220%22%7D  //参数经过encode

ISV返回的结果

正常返回

{
    "code":"200",
    "data":["1", "2", "3"],
    "requestId":"ac100127160126339964722436",
    "message":""
}

异常返回

{
    "code":"Permission Forbidden",
    "data":null,
    "requestId":"ac100127160126339964722436",
    "message":"该用户没有使用权限."
}

阿里云返回给前端 (透传)

ISV 正常返回

{
    "code":"200",
    "data":["1", "2", "3"],
    "requestId":"ac100127160126339964722436",
    "message":""
}

ISV 异常返回

{
    "code":"Permission Forbidden",
    "data":null,
    "requestId":"ac100127160126339964722436",
    "message":"该用户没有使用权限."
}