Appearance
私有应用 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。
验证流程
- 使用
client_credentials向测试环境 Auth 服务兑换 Token。 - 使用 Access Token 调用 OpenAPI
/auth/tokeninfo。 - 校验
application_type=private、actor_type=application。 - 校验 Token 响应和 Token 信息返回相同的
shop_id。
私有应用不需要浏览器授权、PKCE、回调地址或 shop_id 请求参数。Token 刷新和撤销规则见认证与安全。