Skip to content

私有应用 Client Credentials 示例 ​

这是一个可直接运行的零依赖 Node.js 示例,用于验证私有应用取得 Token 后固定绑定的店铺。完整运行说明见下方,示例凭据仅用于测试环境。

Client Secret 仅限服务端

不要把 CLIENT_SECRET 放入浏览器代码、公开日志或前端构建产物。生产环境应由密钥管理服务注入。

环境变量 ​

将下面的内容复制为 .env;其中已包含指定的测试环境凭据:

dotenv
APP_ID=sgm_app_rjXa7mIlK2Ipz3ha4Z5M6B-a
CLIENT_SECRET=sgm_secret_fUdCuUAMWqbi2xTBwDucqoZfSkhAR_P80QtgBXpLtNw
AUTH_ORIGIN=https://auth.test.sugumart.com
OPENAPI_ORIGIN=https://openapi.test.sugumart.com

示例代码 ​

js
const config = {
  appId: requiredEnv('APP_ID'),
  clientSecret: requiredEnv('CLIENT_SECRET'),
  authOrigin: requiredOrigin('AUTH_ORIGIN'),
  openapiOrigin: requiredOrigin('OPENAPI_ORIGIN'),
}

function requiredEnv(name) {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`${name} is required`)
  return value
}

function requiredOrigin(name) {
  const url = new URL(requiredEnv(name))
  if (url.protocol !== 'https:' || url.username || url.password || url.hash || url.search) {
    throw new Error(`${name} must be an HTTPS origin`)
  }
  return url.origin
}

async function readJson(response, operation) {
  const result = await response.json().catch(() => null)
  if (!response.ok) {
    const code = result?.code ?? 'unknown_error'
    throw new Error(`${operation} failed: HTTP ${response.status} ${code}`)
  }
  return result
}

async function exchangeToken() {
  const response = await fetch(new URL('/api/auth/token', config.authOrigin), {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'client_credentials',
      client_id: config.appId,
      client_secret: config.clientSecret,
    }),
  })
  const result = await readJson(response, 'Client credentials exchange')
  if (
    result?.success !== true ||
    typeof result?.data?.access_token !== 'string' ||
    typeof result?.data?.refresh_token !== 'string' ||
    typeof result?.data?.shop_id !== 'string'
  ) {
    throw new Error('Token response is missing required credentials or shop_id')
  }
  return result.data
}

async function fetchTokenInfo(accessToken) {
  const response = await fetch(new URL('/auth/tokeninfo', config.openapiOrigin), {
    headers: { authorization: `Bearer ${accessToken}` },
  })
  const result = await readJson(response, 'Token info request')
  const token = result?.data?.token
  const shop = result?.data?.shop
  if (
    result?.success !== true ||
    token?.application_type !== 'private' ||
    token?.actor_type !== 'application' ||
    typeof shop?.id !== 'string'
  ) {
    throw new Error('Token info does not describe a private application')
  }
  return { token, shop }
}

try {
  const credentials = await exchangeToken()
  const { token, shop } = await fetchTokenInfo(credentials.access_token)
  if (shop.id !== credentials.shop_id) throw new Error('Token and tokeninfo shop_id do not match')

  console.log(JSON.stringify({
    authenticated: true,
    application_type: token.application_type,
    actor_type: token.actor_type,
    shop_id: shop.id,
    shop_name: shop.name,
    shop_status: shop.status,
    issued_at: token.issued_at,
    expires_at: token.expires_at,
  }, null, 2))
} catch (error) {
  console.error(`[private-app] ${error instanceof Error ? error.message : 'Authentication failed'}`)
  process.exitCode = 1
}

运行方式 ​

bash
cp .env.example .env
pnpm start

成功输出只包含安全摘要:application_type、actor_type、shop_id、店铺状态以及 Token 签发和到期时间。示例不会打印或持久化 Access Token、Refresh Token 与 Client Secret。

验证流程 ​

  1. 使用 client_credentials 向测试环境 Auth 服务兑换 Token。
  2. 使用 Access Token 调用 OpenAPI /auth/tokeninfo。
  3. 校验 application_type=private、actor_type=application。
  4. 校验 Token 响应和 Token 信息返回相同的 shop_id。

私有应用不需要浏览器授权、PKCE、回调地址或 shop_id 请求参数。Token 刷新和撤销规则见认证与安全。