开发者集成指南

面向开发者的实用指南,帮助构建与 NBS 平台集成的 Web 或客户端应用程序。


目录

  1. 概述
  2. 开发流程
  3. SSO 集成 (auth.js / NBSAuth)
  4. 文件服务集成 (nbs-file-client.js / NBSFileClient)
  5. 审计服务集成 (REST API)
  6. 认证模式
  7. 基于角色的访问控制
  8. 环境配置
  9. 常见问题排查
  10. 完整示例

1. 概述

NBS 平台为第三方应用提供三项核心服务:

开发者通过两个 JavaScript 库进行集成:

用途
auth.js NBSAuth SSO 认证、令牌管理、会话处理
nbs-file-client.js NBSFileClient 文件上传、下载、删除、列表

注意: 请勿使用 nbs-platform-client.js — 该库仅用于平台管理操作,不适用于第三方应用集成。

审计日志无需 JS 库 — 只需简单的 REST API 调用即可。


2. 开发流程

从注册到上线的分步流程:

步骤 1 — 联系平台管理团队

通过 SIM Ticket(应用上架申请) 提交应用注册申请,需提供以下信息:

信息项 说明 是否必填
应用名称 应用的显示名称 必填
应用描述 简要说明应用功能 必填
分类 data-insights / task-management / workflow-automation / seller-engagement-content-generation / quick-suite-flow-dashboard 必填
入口 URL 用户访问应用的地址,如 http://localhost:9876https://your-app.harmony.a2z.com/。注册时可留空,部署后再更新。 可选
访问模式 public(所有已认证用户)或 whitelist(仅受邀用户) 必填
初始管理员 用户 ID / 登录别名列表 必填

步骤 2 — 应用注册

平台管理员在注册表中创建您的应用,并提供: - appId — kebab-case 格式的标识符(例如 my-cool-app),用于所有 API 调用 - 实验环境 API 端点 URL — 用于集成测试

注意: 生产环境 API 端点将在您准备上线时提供。

步骤 3 — 配置用户和角色

在测试之前,需要配置用户: - 应用管理员(Owner) 可以通过应用管理控制台自行管理用户 — 无需平台管理员参与 - Public 模式: 所有已认证用户均可访问。您仍可为特定用户分配 ownermanager 角色以获得更高权限。 - Whitelist 模式: 应用管理员通过应用管理控制台添加用户。每个用户获得一个角色:ownermanagermember

步骤 4 — 集成平台服务

根据您的应用架构(浏览器端或客户端/服务器端),集成所需的服务。请参阅以下详细指南:

提示: 您的 API 端点应可配置(例如通过 config.json 或环境变量),以便在实验环境和生产环境之间轻松切换。

步骤 5 — 在实验环境中测试

使用实验环境 API 端点进行测试: - SSO 登录/登出流程 - 文件上传、下载、列表 - 审计日志记录 - 基于角色的访问(使用 owner、manager、member 账户测试)

步骤 6 — 发布

两种部署选项:

选项 方式 适用场景
A:平台托管 将源代码提供给平台团队。他们会部署到平台 S3 存储桶的 apps/{appId}/ 目录下,通过 CloudFront 提供服务。 简单的静态 Web 应用,无自定义后端
B:自托管 在您自己的基础设施上托管(Harmony、EC2 等)。将入口 URL 提供给平台管理员以更新应用注册表。 有自定义后端、需求复杂的应用

步骤 7 — 上线

  1. 平台管理员提供生产环境 API 端点
  2. 将应用的 API 端点配置更新为生产环境
  3. 平台管理员在注册表中激活应用
  4. 用户现在可以通过 NBS App Store 访问您的应用

3. SSO 集成

模块作用: SSO(单点登录)集成使您的应用能够利用 NBS 平台的统一身份认证体系,用户无需为每个应用单独注册和登录。通过集成 SSO,您的应用可以获取用户身份信息(姓名、邮箱、角色等),并基于角色控制功能访问权限。

适用场景: - 需要识别用户身份的任何 Web 或客户端应用 - 需要基于角色(owner/manager/member)展示不同功能的应用 - 需要调用 NBS 文件服务或审计服务的应用(这些服务要求 SSO 认证) - 后端自动化服务需要以服务身份调用 NBS API(M2M 场景)

NBS 支持两种认证路径,适用于不同场景:

认证路径 令牌类型 使用场景 适用对象
Federate OAuth 2.0 PKCE Federate JWT (RS256) 交互式用户会话 — 用户通过浏览器登录 Web 应用、SPA、任何用户点击"登录"的 UI
Cognito M2M (Client Credentials) Cognito Access Token (RS256) 服务间调用 — 无人工用户参与 后端服务、定时任务、CI/CD 流水线、自动化脚本

3.1 Federate 认证 — 浏览器端(Web 应用)

适用于用户通过浏览器交互登录的 Web 应用。auth.js 提供了 NBSAuth 类,处理完整的 OAuth 2.0 PKCE 流程。

平台托管应用(部署在 /apps/{appId}/ 下):使用平台统一回调地址 /callback.html。这样无需为每个子应用在 Federate 中单独注册回调 URL — 所有平台托管应用只需一个回调地址。

自托管应用(部署在自己的域名上):使用自己页面的 URL 作为回调地址,并在 Federate 中注册。

最简配置

<script src="./auth.js"></script>
<script>
async function initApp() {
    // 1. 加载配置
    const resp = await fetch('./config.json');
    const config = await resp.json();

    // 2. 创建认证实例
    const auth = new NBSAuth({
        apiEndpoint: config.api_gateway_url,
        redirectUri: window.location.origin + '/callback.html',  // 平台统一回调地址
        appId: 'your-app-id',
        appName: 'Your App Name'
    });

    // 3. 初始化(从后端获取 OAuth 配置)
    await auth.initialize();

    // 4. 检查认证状态(令牌通过 sessionStorage 从平台回调页共享)
    if (!auth.isAuthenticated()) {
        await auth.login(); // 重定向到 OAuth → 平台回调 → 返回本页面
        return;
    }

    // 5. 验证此应用的令牌(服务器端验证)
    const userInfo = await auth.verifyToken('your-app-id');

    // 5. 检查认证状态
    if (!auth.isAuthenticated()) {
        await auth.login(); // 重定向到 OAuth 登录页面
        return;
    }

    // 6. 验证此应用的令牌(服务器端验证)
    const userInfo = await auth.verifyToken('your-app-id');
    // userInfo: { sub, appId, userRole, fullname, email, effectiveRole, activeDelegations }

    // 7. 应用就绪
    console.log('Welcome', userInfo.fullname);
}

initApp();
</script>

生产就绪模式(带遮罩层 UI)

为了提供更好的用户体验,使用登录遮罩层在认证过程中显示状态信息:

<script src="./auth.js"></script>

<!-- 登录遮罩层 -->
<div id="ssoOverlay" style="display:none; position:fixed; inset:0; background:rgba(245,245,247,0.97);
    backdrop-filter:blur(20px); z-index:99999; align-items:center; justify-content:center; flex-direction:column;">
    <div style="text-align:center; max-width:400px; padding:48px 40px; background:white;
        border-radius:24px; box-shadow:0 8px 40px rgba(0,0,0,0.12);">
        <div style="font-size:48px; margin-bottom:16px;">🔐</div>
        <h2 style="font-size:24px; font-weight:600; margin-bottom:8px;">Your App Name</h2>
        <p id="ssoStatusMsg" style="font-size:16px; color:#515154; margin-bottom:32px;">Verifying identity...</p>
        <button id="ssoLoginBtn" onclick="window._auth && window._auth.login()" style="display:none;
            width:100%; background:#0071e3; color:white; border:none; border-radius:12px;
            padding:16px 24px; font-size:16px; font-weight:600; cursor:pointer;">
            Sign In with SSO
        </button>
    </div>
</div>

<script>
const APP_ID = 'your-app-id';
const APP_NAME = 'Your App Name';

function showOverlay(msg, showBtn) {
    const overlay = document.getElementById('ssoOverlay');
    overlay.style.display = 'flex';
    document.getElementById('ssoStatusMsg').textContent = msg;
    document.getElementById('ssoLoginBtn').style.display = showBtn ? 'block' : 'none';
}

function hideOverlay() {
    document.getElementById('ssoOverlay').style.display = 'none';
}

document.addEventListener('DOMContentLoaded', async () => {
    try {
        showOverlay('Loading configuration...', false);
        const resp = await fetch('./config.json');
        const config = await resp.json();

        const auth = new NBSAuth({
            apiEndpoint: config.api_gateway_url,
            redirectUri: window.location.origin + '/callback.html',  // 平台统一回调地址
            appId: APP_ID,
            appName: APP_NAME,
            oauthConfig: config.oauth_config  // 从 config.json 加载(按环境部署)
        });
        window._auth = auth;

        await auth.initialize();

        if (auth.isAuthenticated()) {
            showOverlay('Verifying identity...', false);
            try {
                const userInfo = await auth.verifyToken(APP_ID);
                hideOverlay();
                onAppReady(auth, userInfo, config);
            } catch (e) {
                if (e.message === 'TOKEN_EXPIRED') {
                    await auth.refreshAccessToken();
                    const userInfo = await auth.verifyToken(APP_ID);
                    hideOverlay();
                    onAppReady(auth, userInfo, config);
                } else if (e.message === 'FORBIDDEN_ACCESS') {
                    showOverlay('You do not have access to this app.', false);
                    return;
                } else {
                    throw e;
                }
            }
        } else {
            showOverlay('Please sign in to continue.', true);
            return;
        }
    } catch (err) {
        console.error('SSO init failed:', err);
        showOverlay('Initialization failed. Please refresh.', true);
    }
});

function onAppReady(auth, userInfo, config) {
    console.log('Welcome', userInfo.fullname, '| Role:', userInfo.userRole);
    // 您的应用逻辑从这里开始
}
</script>

NBSAuth API 参考

构造函数

const auth = new NBSAuth({
    apiEndpoint: 'https://...',    // 必填 — API Gateway 端点 URL
    redirectUri: '...',            // 必填 — OAuth 重定向 URI(平台托管应用使用 '/callback.html')
    appId: 'my-app',              // 可选 — 用于登录追踪的应用 ID
    appName: 'My App',            // 可选 — 用于登录追踪的应用名称
    usePersistentStorage: false    // 可选 — true = localStorage,false = sessionStorage(默认)
});

方法

Method Returns 描述
initialize() Promise<void> 验证构造函数中是否传入了 oauthConfig,缺失时抛出错误。不发起网络请求 — OAuth 配置在启动时从 config.json 加载。
login() Promise<void> 将浏览器重定向到 OAuth 授权页面(PKCE 流程)。
handleCallback() Promise<boolean> 用授权码交换令牌。成功返回 true
isAuthenticated() boolean 检查存储中是否存在令牌(不进行验证)。
verifyToken(appId) Promise<Object> 调用 /auth/verify-token — 从后端返回可信的 user_info推荐的应用访问检查方式。
getValidAccessToken() Promise<string> 返回有效的访问令牌。如果令牌将在 5 分钟内过期,自动刷新。
getAccessToken() string\|null 返回当前访问令牌,不检查是否需要刷新。
getUserInfo() Object\|null 返回上次令牌交换/验证时缓存的 user_info 对象。
refreshAccessToken() Promise<Object> 手动刷新访问令牌。使用互斥锁防止并发刷新。
isTokenExpired(bufferSeconds) boolean 如果令牌将在 bufferSeconds 秒内过期,返回 true
ensureValidSession(appId) Promise<boolean> 组合检查:验证 + 自动刷新 + 失败时自动重定向。
logout() void 清除存储中的所有令牌并重定向到登录页面。

verifyToken 响应对象

{
    sub: 'user-id',              // 用户标识符
    appId: 'my-app',            // 验证令牌所针对的应用
    userRole: 'member',          // 在此应用中的角色:'owner'、'manager' 或 'member'
    fullname: 'John Doe',       // 显示名称
    email: 'john@example.com',  // 电子邮件地址
    effectiveRole: 'member',    // 有效角色(考虑委托关系)
    activeDelegations: []       // 活跃的委托条目
}

重要说明

3.2 Federate 认证 — 服务器端(客户端/服务器应用)

适用于有后端服务器(Node.js、Python、Java 等)的应用,服务器直接验证 Federate JWT 并代表用户调用 NBS API。

流程:

浏览器 → 您的后端 → NBS API(携带 Bearer 令牌)

工作原理:

  1. 您的前端获取 Federate 访问令牌(在浏览器中使用 NBSAuth,或使用您自己的 OAuth 流程)
  2. 前端将令牌发送到您的后端(例如通过 Cookie 或 Authorization 头)
  3. 您的后端在调用 NBS API 时携带该令牌:
# Python 示例 — 从后端调用 NBS API
import requests

NBS_API = "https://your-api-endpoint.execute-api.region.amazonaws.com/prod"

def list_files(access_token, app_id, directory=""):
    resp = requests.get(
        f"{NBS_API}/apps/{app_id}/files",
        headers={"Authorization": f"Bearer {access_token}"},
        params={"directory": directory}
    )
    resp.raise_for_status()
    return resp.json()

def log_audit(access_token, app_id, operation, details=None):
    resp = requests.post(
        f"{NBS_API}/apps/{app_id}/audit/log",
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        },
        json={
            "operation": operation,
            "status": "success",
            "operationDetails": details or {}
        }
    )
    resp.raise_for_status()
    return resp.json()

要点: - NBS API Gateway Lambda Authorizer 会验证 Federate JWT — 您的后端无需自行验证令牌 - 令牌中的 sub 声明标识用户身份 - 所有 NBS API 端点接受 Authorization: Bearer <federate_access_token> - 令牌刷新由您的后端负责 — 当访问令牌过期时,使用刷新令牌调用 POST /auth/refresh-token

适用于 Python 桌面应用或 CLI 工具,无需浏览器或 localhost 回调服务器。利用 mwinit 生成的 Midway cookie 自动完成 OAuth 认证流程。

前提条件:用户需安装 mwinit 并至少运行一次(Amazon 开发者标准配置)。

工作原理:

  1. ~/.midway/cookie 读取 Midway cookie
  2. 携带 cookie 和 PKCE 参数请求 Federate 的 /authorize 端点
  3. Federate 通过 Midway cookie 自动认证,返回 302 重定向(包含授权码)
  4. 从重定向 URL 中提取授权码(无需 localhost 服务器)
  5. 通过 Lambda /auth/exchange-token API 交换令牌
import os, re, requests
from http.cookiejar import MozillaCookieJar
import base64, hashlib

# PKCE helper
def generate_pkce():
    verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
    challenge = base64.urlsafe_b64encode(
        hashlib.sha256(verifier.encode()).digest()
    ).rstrip(b"=").decode()
    return verifier, challenge

NBS_API = "https://your-api-endpoint.execute-api.region.amazonaws.com/prod"

# OAuth 配置 — 不再从 /auth/config 获取(该端点已移除)。
# 根据环境使用对应的值:
#   Staging:    client_id="nbsaihubstaging", authorization_endpoint="https://idp-integ.federate.amazon.com/api/oauth2/v1/authorize"
#   Production: client_id="ai-app-store",    authorization_endpoint="https://idp.federate.amazon.com/api/oauth2/v1/authorize"
OAUTH_CONFIG = {
    "client_id": "ai-app-store",
    "authorization_endpoint": "https://idp.federate.amazon.com/api/oauth2/v1/authorize",
    "scope": "openid profile email",
    "redirect_uri": "https://ai.wwgs.amazon.dev/callback.html"
}

def authenticate_with_midway_cookie():
    # 1. 使用静态 OAuth 配置(从环境配置文件加载)
    config = OAUTH_CONFIG
    redirect_uri = config["redirect_uri"]  # Platform callback URL
    
    # 2. Load Midway cookie
    jar = MozillaCookieJar(os.path.expanduser("~/.midway/cookie"))
    jar.load(ignore_discard=True, ignore_expires=True)
    cookies = requests.cookies.RequestsCookieJar()
    for c in jar:
        cookies.set_cookie(c)
    
    # 3. Generate PKCE and request authorization
    verifier, challenge = generate_pkce()
    session = requests.Session()
    session.cookies = cookies
    resp = session.get(config["authorization_endpoint"], params={
        "client_id": config["client_id"],
        "response_type": "code",
        "scope": config.get("scope", "openid"),
        "redirect_uri": redirect_uri,
        "code_challenge": challenge,
        "code_challenge_method": "S256",
    }, allow_redirects=True)
    
    # 4. Extract code from final URL
    code = re.search(r"[?&]code=([^&]+)", resp.url)
    if not code:
        raise Exception("Authentication failed — run mwinit and retry")
    
    # 5. Exchange code for tokens
    tokens = requests.post(f"{NBS_API}/auth/exchange-token", json={
        "code": code.group(1),
        "code_verifier": verifier,
        "redirect_uri": redirect_uri,
    }).json()
    
    return tokens  # {access_token, id_token, refresh_token, user_info}

要点:

3.3 Cognito M2M 认证(服务间通信)

适用于自动化服务、定时任务、CI/CD 流水线,或任何无人工用户参与的场景。

何时使用 M2M: - 后端批处理(例如每晚生成报表) - CI/CD 流水线将构建产物上传到 NBS 文件存储 - 监控系统的自动化审计日志记录 - 服务间集成,您的后端在无用户会话的情况下调用 NBS API

前提条件: - 联系平台管理团队获取 Cognito M2M 客户端凭证(client_id + client_secret) - M2M 客户端已在与 NBS 平台关联的 Cognito User Pool 中注册

步骤 1 — 获取 M2M 访问令牌:

import requests

COGNITO_TOKEN_ENDPOINT = "https://your-cognito-domain.auth.region.amazoncognito.com/oauth2/token"
CLIENT_ID = "your-m2m-client-id"
CLIENT_SECRET = "your-m2m-client-secret"

def get_m2m_token():
    resp = requests.post(
        COGNITO_TOKEN_ENDPOINT,
        data={
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "scope": "openid"
        },
        headers={"Content-Type": "application/x-www-form-urlencoded"}
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

步骤 2 — 使用 auth_source=cognito_m2m 调用 NBS API:

NBS_API = "https://your-api-endpoint.execute-api.region.amazonaws.com/prod"

def upload_file_m2m(token, app_id, directory, filename, content_type):
    """通过 M2M 认证获取预签名上传 URL"""
    resp = requests.post(
        f"{NBS_API}/apps/{app_id}/files/upload?auth_source=cognito_m2m",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        },
        json={
            "directory": directory,
            "filename": filename,
            "contentType": content_type
        }
    )
    resp.raise_for_status()
    return resp.json()  # 包含 uploadUrl、requiredHeaders、s3Key

def log_audit_m2m(token, app_id, user_id, operation, details=None):
    """代表用户记录审计事件(M2M 必须在请求体中提供 userId)"""
    resp = requests.post(
        f"{NBS_API}/apps/{app_id}/audit/log?auth_source=cognito_m2m",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        },
        json={
            "operation": operation,
            "status": "success",
            "operationDetails": details or {},
            "userId": user_id  # M2M 必填 — 标识最终用户
        }
    )
    resp.raise_for_status()
    return resp.json()

Federate 认证与 M2M 认证的主要区别:

方面 Federate(用户) Cognito M2M(服务)
查询参数 无(默认) ?auth_source=cognito_m2m
用户身份 从令牌 sub 中提取 必须在请求体中提供 userId(用于审计)
令牌来源 OAuth 2.0 PKCE 流程(浏览器) Client Credentials Grant(服务器)
令牌刷新 POST /auth/refresh-token 从 Cognito 请求新令牌
使用场景 交互式用户会话 自动化/后台处理

4. 文件服务集成

模块作用: 文件服务为您的应用提供安全的、按应用隔离的文件存储能力。每个应用拥有独立的文件空间,支持基于角色的目录访问控制、文件上传/下载/删除、软删除与恢复、分页列表和通配符搜索。所有文件操作通过 S3 Presigned URL 完成,无需直接访问 S3。

适用场景: - 需要为用户提供个人文件存储空间的应用(如文档管理、报表系统) - 需要团队共享文件的协作应用(通过 .public/ 目录) - 需要 Owner 分发文件给团队成员的管理应用 - 需要批量文件处理的自动化服务(通过 M2M 认证)

nbs-file-client.js 提供了 NBSFileClient 类,用于所有文件操作。它需要一个 NBSAuth 实例进行认证。

配置

<script src="./auth.js"></script>
<script src="./nbs-file-client.js"></script>
<script>
// SSO 初始化完成后:
const fileClient = new NBSFileClient(config.api_gateway_url, auth);
</script>

NBSFileClient API 参考

构造函数

const fileClient = new NBSFileClient(apiEndpoint, authClient);
// apiEndpoint — API Gateway 端点 URL(字符串)
// authClient  — 一个 NBSAuth 实例(必须已初始化并完成认证)

方法

Method Returns 描述
getUserApps() Promise<{ apps, role, totalApps }> 列出用户可访问的所有应用。
uploadFile(appId, path, file) Promise<{ key, message }> 上传文件。path 格式:"directory/filename""filename"
downloadFile(appId, path) Promise<Blob> 下载文件。返回 Blob 对象。
deleteFile(appId, path) Promise<{ message }> 软删除文件。
listFiles(appId, options) Promise<Object> 列出文件,支持过滤和分页。
listTeamSharedFiles(appId) Promise<Array> 列出 .public/ 目录中的文件。
getUserRole(appId) Promise<string\|null> 返回 'owner''manager''member'null

listFiles 选项

await fileClient.listFiles('my-app', {
    directory: 'user1',           // 目录路径(默认:根目录)
    recursive: false,             // 递归列出所有文件(仅 owner)
    search: '*report*.xlsx',      // 通配符搜索模式
    includeDeleted: false,        // 包含已软删除的文件(仅 owner)
    maxKeys: 1000,                // 每页最大结果数
    continuationToken: null       // 上一次响应的分页令牌
});

目录结构

每个应用在 S3 中的目录布局如下:

{appId}/
├── .private/              # 仅 Owner 可读写
├── .public/               # 所有角色可读;仅 Owner 可写
├── {userId}/              # 个人目录(每个用户一个)
│                          #   owner:可访问所有用户目录
│                          #   manager:可访问自己 + 下属的目录
│                          #   member/team_member:仅可访问自己的目录
├── .teams/{teamId}/       # 团队目录(绑定团队时自动创建)
│   ├── .public/           #   团队成员可读;team_leader/team_owner 可读写
│   └── .private/          #   仅 team_leader/team_owner 可读写
└── .teams/{subTeamId}/    # 子团队目录
    └── .public/           #   子团队成员可读;父团队 .public 需 allowChildAccessToDir=true

文件上传示例

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const userId = auth.getUserInfo().sub;

try {
    const result = await fileClient.uploadFile('my-app', `${userId}/${file.name}`, file);
    console.log('Uploaded:', result.key);
} catch (err) {
    console.error('Upload failed:', err.message);
}

文件下载示例

async function downloadFile(appId, filePath, downloadName) {
    const blob = await fileClient.downloadFile(appId, filePath);
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = downloadName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

// 用法
await downloadFile('my-app', `${userId}/report.xlsx`, 'report.xlsx');

文件列表示例

// 列出用户自己目录中的文件
const result = await fileClient.listFiles('my-app', { directory: userId });
console.log('Files:', result.files);
console.log('Folders:', result.folders);

// 分页列表
let token = null;
do {
    const page = await fileClient.listFiles('my-app', {
        directory: userId,
        maxKeys: 100,
        continuationToken: token
    });
    console.log('Page:', page.files);
    token = page.isTruncated ? page.nextContinuationToken : null;
} while (token);

Presigned URL 的 IP 限制

当平台启用了 IP 限制功能时,presigned URL 会被锁定到客户端的 IP 地址(CIDR /24 范围)。这可以防止 URL 被分享——为某个用户生成的 presigned URL 如果从其他 IP 访问会返回 403。

浏览器端应用: 无需额外处理。平台会自动从请求中提取客户端 IP 并限制 presigned URL。

客户端/服务器应用: 您的后端必须将终端用户的真实 IP 转发到 NBS API,以确保 presigned URL 被限制到正确的 IP。通过传递 X-Forwarded-For 头实现:

# Python 后端示例 — 将客户端 IP 转发到 NBS API
def get_download_url(user_token, app_id, directory, filename, client_ip):
    resp = requests.post(
        f"{NBS_API}/apps/{app_id}/files/download",
        headers={
            "Authorization": f"Bearer {user_token}",
            "Content-Type": "application/json",
            "X-Forwarded-For": client_ip  # 转发终端用户的真实 IP
        },
        json={"directory": directory, "filename": filename}
    )
    return resp.json()["downloadUrl"]

如果您的后端有自己的 API Gateway 并附加了 Lambda Authorizer: API Gateway 可能会在 authorizer 和主 Lambda 之间修改 X-Forwarded-For 头。为确保原始客户端 IP 被保留,让您的 authorizer 在返回的 context 中传递原始 IP(例如 originalXFForiginalSourceIp 字段)。NBS 平台自带的 authorizer Lambda 已经实现了这一点——如果您使用的是 NBS authorizer Lambda,则无需额外操作。

关键规则: 请求链路中的每个代理或网关都必须通过 X-Forwarded-For 转发原始客户端 IP。如果任何一个环节丢弃了这个头,presigned URL 将被限制到错误的 IP,终端用户的下载将返回 403 错误。

自动 401 重试

所有 NBSFileClient 方法自动处理令牌过期。收到 401 响应时,客户端会: 1. 调用 auth.refreshAccessToken() 获取新令牌 2. 使用新令牌重试原始请求一次

您的应用代码中无需手动编写重试逻辑。


5. 审计服务集成

模块作用: 审计服务为您的应用提供结构化的操作日志记录能力,用于合规审计和问题追溯。所有审计记录统一存储在 NBS 平台的审计日志表中,平台管理员可通过审计控制台查询和分析。

适用场景: - 需要记录用户关键操作(如查看报表、导出数据、修改配置)的合规应用 - 需要追踪文件访问记录的安全敏感应用 - 需要从后端服务记录操作日志的自动化系统(通过 M2M 认证)

审计服务是一个 REST API — 无需 JS 库。直接使用 fetch 调用即可。

端点

POST /apps/{appId}/audit/log

认证

在请求头中包含访问令牌作为 Bearer 令牌:

Authorization: Bearer <access_token>

请求体

{
    "operation": "view_report",
    "status": "success",
    "operationDetails": { "reportId": "R-001", "format": "pdf" },
    "filePath": "user1/reports/Q1.pdf",
    "errorMessage": ""
}

字段验证规则

字段 必填 约束
operation 正则:^[a-zA-Z0-9_:./-]{1,100}$
status 可选值:successfailederrorpartial。默认:success
operationDetails JSON 对象,序列化后最大 4 KB
filePath 最大 500 个字符,不含控制字符
errorMessage 最大 1000 个字符

辅助函数

/**
 * 为当前用户记录审计事件。
 *
 * @param {NBSAuth} auth       - 已认证的 NBSAuth 实例
 * @param {string} apiEndpoint - API Gateway URL
 * @param {string} appId       - 您的应用 ID
 * @param {string} operation   - 操作名称(例如 'view_report'、'export_data')
 * @param {Object} [details]   - 可选的操作详情
 * @param {string} [status]    - 可选的状态(默认:'success')
 */
async function logAudit(auth, apiEndpoint, appId, operation, details = {}, status = 'success') {
    const token = await auth.getValidAccessToken();
    const response = await fetch(`${apiEndpoint}/apps/${appId}/audit/log`, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            operation,
            status,
            operationDetails: details
        })
    });

    if (!response.ok) {
        console.error('Audit log failed:', response.status);
    }
}

使用示例

// 记录成功的报表查看
await logAudit(auth, apiEndpoint, 'my-app', 'view_report', {
    reportId: 'R-001',
    format: 'pdf'
});

// 记录失败的导出
await logAudit(auth, apiEndpoint, 'my-app', 'export_data', {
    format: 'csv',
    recordCount: 5000
}, 'failed');

// 记录带文件路径的操作
const token = await auth.getValidAccessToken();
await fetch(`${apiEndpoint}/apps/my-app/audit/log`, {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        operation: 'download_file',
        status: 'success',
        filePath: 'user1/reports/Q1-2024.xlsx',
        operationDetails: { fileSize: '2.4MB' }
    })
});

响应

{
    "message": "Audit log recorded",
    "userId": "user1",
    "appId": "my-app",
    "operation": "view_report",
    "timestamp": "2024-01-15T08:30:00.000Z"
}

6. 认证模式

模式 行为 配置
Public 所有已认证的 NBS 用户均可访问应用。用户默认获得 member 角色。应用管理员仍可为特定用户分配 ownermanager 角色以获得更高权限。 平台管理员在注册时将访问模式设置为 public
Whitelist 仅明确添加的用户可以访问。未授权用户将收到 403 FORBIDDEN_ACCESS 错误。 平台管理员将访问模式设置为 whitelist。应用管理员(owner)通过应用管理控制台管理用户 — 日常用户管理无需平台管理员参与。

7. 基于角色的访问控制

角色概览

角色 文件访问权限 描述
owner 完全访问所有目录,包括 .private/ 应用管理员 — 可查看所有用户目录,全局管理文件
manager .private/ 外的所有目录 团队负责人 — 可查看所有用户目录,但无法访问私有存储
member 仅限自己的 {userId}/ 目录和 .public/ 普通用户 — 个人存储和共享文件
member(团队绑定) 同 member 通过团队绑定自动获得访问权限,无需手动添加
team_owner 同 member + 可查看团队成员 管理指定团队的成员和配置,由管理员分配

角色由平台管理员或应用 owner 分配。角色信息通过 verifyToken()userRole 字段返回。

团队权限集成

NBS 平台支持基于团队的访问控制。当团队绑定到应用后,团队成员自动获得应用访问权限。

// 获取用户在某个 app 中的权限和能力
const resp = await fetch(`${API}/apps/${appId}/users/${userId}/permissions`, {
  headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();
// data.accessVia: 'personal_permission' 或 'team_binding'
// data.capabilities: [{ type: 'team_leader', teamId: '...', teamName: '...' }]

// 搜索团队(最少 2 个字符)
const searchResp = await fetch(`${API}/teams/search?query=nbs`, {
  headers: { 'Authorization': `Bearer ${token}` }
});
const { teams } = await searchResp.json();
// teams: [{ teamId, teamName, parentTeamId, depth }]

注意:团队绑定和解绑操作(POST/DELETE /apps/{appId}/teams)仅限 App Admin 或 Platform Admin 使用,详见 API 文档 Section 8


8. 环境配置

环境 用途 API 端点 Federate client_id Federate 端点
Staging / 实验环境 集成测试、开发 https://nbsaihub.ags.amazon.dev/prod nbsaihubstaging idp-integ.federate.amazon.com
生产环境 正式用户、真实数据 https://ai.wwgs.amazon.dev/prod ai-app-store idp.federate.amazon.com

通过更新 config.json 切换环境,oauth_config 字段必须与环境匹配:

// Staging / 实验环境
{
    "api_gateway_url": "https://nbsaihub.ags.amazon.dev/prod",
    "oauth_config": {
        "client_id": "nbsaihubstaging",
        "authorization_endpoint": "https://idp-integ.federate.amazon.com/api/oauth2/v1/authorize",
        "scope": "openid profile email",
        "response_type": "code"
    }
}

→ 切换到生产环境:

// 生产环境
{
    "api_gateway_url": "https://ai.wwgs.amazon.dev/prod",
    "oauth_config": {
        "client_id": "ai-app-store",
        "authorization_endpoint": "https://idp.federate.amazon.com/api/oauth2/v1/authorize",
        "scope": "openid profile email",
        "response_type": "code"
    }
}

提示: 保留单独的 config-experimental.jsonconfig-production.json 文件,在部署时将相应文件复制为 config.json

OAuth 配置说明

GET /auth/config 端点已移除。OAuth 配置现在嵌入在 config.json 中(按环境部署到 S3)。各环境配置值如下:

字段 Staging Production
client_id nbsaihubstaging ai-app-store
authorization_endpoint https://idp-integ.federate.amazon.com/api/oauth2/v1/authorize https://idp.federate.amazon.com/api/oauth2/v1/authorize
scope openid profile email openid profile email
redirect_uri https://nbsaihub.ags.amazon.dev/callback.html https://ai.wwgs.amazon.dev/callback.html

oauth_configconfig.json 传入 NBSAuth 构造函数,无需单独的 API 调用。


9. 常见问题排查

问题 原因 解决方案
401 Unauthorized 令牌过期或无效 NBSAuth 会自动刷新令牌。如果问题持续,调用 auth.login() 重新认证。
403 Forbidden 用户不在白名单中 联系平台管理员将用户添加到应用白名单。
CORS 错误 您的域名不在允许的来源列表中 联系平台管理员将您的域名添加到 API Gateway CORS 配置中。
verifyToken 抛出 FORBIDDEN_ACCESS 应用处于 whitelist 模式且用户无权限 显示"无访问权限"提示。用户需由平台管理员或应用 owner 添加。
verifyToken 抛出 TOKEN_EXPIRED 访问令牌已过期 调用 auth.refreshAccessToken() 然后重试 verifyToken()
verifyToken 抛出 SYSTEM_ERROR 后端错误或网络问题 短暂延迟后重试。如果问题持续,检查 API 端点配置和网络连接。
文件上传失败 路径格式不正确或权限不足 确保路径格式为 "directory/filename"。Member 只能上传到自己的 {userId}/ 目录或 .public/
config.json 加载失败 路径错误或 CORS 问题 确保 config.json 与 HTML 文件在同一目录下。检查浏览器开发者工具的网络标签页。

10. SDK 参考包与示例代码

我们提供了一个完整的 SDK 参考包(位于 bs-sdk/ 目录),包含可运行的示例应用,替代内联代码示例。

SDK 目录结构

nbs-sdk/ ├── README.md # 快速开始指南 ├── KIRO_PROMPTS.md # AI 辅助集成提示词(见下方) ├── lib/ # 共享 JS 库(复制到您的项目) │ ├── auth.js # NBSAuth — OAuth 2.0 PKCE 认证 │ └── nbs-file-client.js # NBSFileClient — 文件操作 ├── browser-app/ # 浏览器端 (BS) 示例 │ ├── config.json # API 端点配置 │ └── index.html # 完整演示:SSO + 文件 + 审计 └── server-app/ # 客户端/服务器 (CS) 示例 ├── config.json # API 端点配置 ├── index.html # 前端:SSO 登录,调用后端 └── server.py # Python 后端:Federate 中转 + M2M

浏览器端示例 (browser-app/)

单个 HTML 文件演示了三项集成(SSO + 文件 + 审计),完全在浏览器中运行。试用方法:

  1. 将 config.json 中的 API 端点更新为您的实验环境地址
  2. 将 index.html 中的 APP_ID 改为您注册的 appId
  3. 运行:python -m http.server 9876(在 browser-app/ 目录下)
  4. 打开 http://localhost:9876

客户端/服务器示例 (server-app/)

前端 + Python 后端演示两种模式: - Federate 中转: 前端做 SSO 登录,将令牌传给后端,后端中转到 NBS API - M2M(机器对机器): 后端使用自己的 Cognito 凭证调用 NBS API,无需用户会话

试用方法: 1. 更新两个 config.json 的 API 端点 2. 设置环境变量:NBS_API_URL、COGNITO_TOKEN_ENDPOINT、COGNITO_CLIENT_ID、COGNITO_CLIENT_SECRET 3. 运行:pip install requests flask flask-cors && python server.py 4. 打开 http://localhost:9876 访问前端

使用 Kiro 进行 AI 辅助集成

SDK 包含 KIRO_PROMPTS.md,提供了可直接使用的 Kiro(或其他 AI 编程助手)提示词。在编辑器中打开相关示例文件,然后复制粘贴对应的提示词:

提示词 场景 参考文件
Prompt 1 在 Web 应用中集成 Federate SSO(浏览器端) lib/auth.js, browser-app/index.html
Prompt 2 在客户端/服务器应用中集成 Federate SSO lib/auth.js, server-app/index.html, server-app/server.py
Prompt 3 集成 Cognito M2M 认证(服务间通信) server-app/server.py
Prompt 4 集成 NBS 文件服务 lib/nbs-file-client.js, browser-app/index.html
Prompt 5 集成 NBS 审计服务 browser-app/index.html, server-app/server.py
Prompt 6 全量集成(SSO + 文件 + 审计) browser-app/ 或 server-app/ 全部文件

提示: 在发送提示词之前,先在编辑器中打开上表列出的参考文件,这样 Kiro 就能获取生成准确代码所需的上下文。记得将提示词中的 {YOUR_APP_ID} 等占位符替换为您的实际值。


快速参考

config.json                → { "api_gateway_url": "https://..." }
auth.js                    → NBSAuth 类(SSO、令牌、会话)
nbs-file-client.js         → NBSFileClient 类(文件操作)
POST /apps/{appId}/audit/log → 审计日志(REST,无需库)

最小集成清单:

步骤 操作 必要性
1 复制 auth.js + config.json 必须
2 初始化 NBSAuth,处理回调,验证令牌 必须
3 复制 nbs-file-client.js,创建 NBSFileClient 可选(需要文件服务时)
4 调用审计 REST API 记录操作日志 可选(需要审计时)
5 在实验环境中测试 必须
6 切换到生产环境端点并上线 必须