第三方接入代码示例
本文提供获取 Access Token、调用 API、接入 PKCE 登录和接收 Webhook 的代码示例。接入前,请在开发者后台创建应用并开通所需 API 权限。
示例使用占位值。请从当前应用复制 Issuer、Client ID、API 地址、Audience 和 Scope,并填入服务端配置。
一、选择调用身份
| 场景 | 调用身份 | 认证方式 | Client Secret |
|---|---|---|---|
| 后台任务、定时同步、服务间调用 | 应用身份权限 | Client Credentials | 仅服务端保存 Client Secret |
| 用户登录后代表用户调用 | 用户身份权限 | Authorization Code + PKCE(S256) | 无 Client Secret |
如果两种场景都需要,就在同一个 Application 中分别启用、分别申请权限。两种身份的 Client、Scope 和 Token 不能混用。
二、从控制台复制配置
| 控制台页面 | 配置项 | 用途 |
|---|---|---|
| OAuth/OIDC 配置 | Issuer、Client ID、回调地址 | 登录和获取 Token |
| API 与权限 | API Base、Audience、已批准 Scope、接口路径 | 服务端调用 API |
| 事件与 Webhook | Webhook 地址、事件类型、密钥交付状态 | 接收订阅和支付事件 |
只复制当前环境、当前身份已经批准的内容。用户身份不需要 Client Secret;应用身份的 Secret 只能放在服务端的 Secret Manager 中。
三、服务端配置示例
GOODVISION_IDENTITY_ISSUER=<从 OAuth/OIDC 配置复制>
GOODVISION_TOKEN_ENDPOINT=<Discovery token_endpoint>
GOODVISION_AUTHORIZATION_ENDPOINT=<Discovery authorization_endpoint>
GOODVISION_APPLICATION_ID=<当前 Application ID>
GOODVISION_API_BASE=<从 API 与权限复制>
GOODVISION_AUDIENCE=<从 API 与权限复制>
GOODVISION_SCOPES=<已批准的最小 Scope,用空格分隔>
# 只有应用身份需要下面两项;不要放进浏览器或前端代码。
GOODVISION_SERVICE_CLIENT_ID=<应用身份 Client ID>
GOODVISION_SERVICE_CLIENT_SECRET=<应用身份 Client Secret>使用用户身份时,改为配置 GOODVISION_USER_CLIENT_ID、已经登记的 GOODVISION_REDIRECT_URI 和 GOODVISION_POST_LOGOUT_URI,不填写 Client Secret。
用精确 GOODVISION_IDENTITY_ISSUER 加 /.well-known/openid-configuration 读取 Discovery,核对 issuer,使用返回的 token_endpoint、authorization_endpoint 及按需使用的 end_session_endpoint。上面的 Endpoint 变量保存这些返回地址,不猜固定路径。
推荐事件接收可直接运行Node.js 22 长连接示例;HTTPS 接收与同步回调分别配置。
四、应用身份:获取 Token
这一步只能在第三方后端执行。浏览器不能执行 Client Credentials,也不能接收 Client Secret。
curl --request POST \
--url "${GOODVISION_TOKEN_ENDPOINT}" \
--header 'content-type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials' \
--data-urlencode "client_id=${GOODVISION_SERVICE_CLIENT_ID}" \
--data-urlencode "client_secret=${GOODVISION_SERVICE_CLIENT_SECRET}"服务端使用返回的短期 Token 调用 API;同时检查签发方、目标 API、有效期和 Scope。Token 过期后重新获取,不要自行延长有效期。
五、调用 API
从当前应用已开通的 API 产品获取 GOODVISION_API_BASE 和接口路径。下面以 SaaSdemo Sandbox 商业接口为例:
curl --request GET \
--url "${GOODVISION_API_BASE}/open-platform/v1/products" \
--header "authorization: Bearer ${GOODVISION_ACCESS_TOKEN}" \
--header "X-GV-Application-ID: ${GOODVISION_APPLICATION_ID}"| 接口 | 需要的最小 Scope |
|---|---|
GET /open-platform/v1/products | commerce.catalog.read |
POST /open-platform/v1/checkout-sessions | commerce.checkout.create |
GET /open-platform/v1/operations/{id} | commerce.checkout.read |
GET /open-platform/v1/subscriptions、GET /open-platform/v1/subscriptions/{id} | commerce.subscription.read |
POST /open-platform/v1/subscriptions/{id}:cancel、:change-plan | commerce.subscription.manage |
只有申请并获批对应 API Product、Audience 和 Scope 后,接口才可以调用。所有写请求都要带唯一的 Idempotency-Key;修改已有资源时还要带当前版本的 If-Match。
金额、币种、支付渠道、组织 ID 和回跳地址不能由第三方前端或请求参数覆盖。订阅、支付、账单和权益页面仍由 GoodVision 客户工作区负责。
六、用户登录:PKCE
网页登录使用用户身份。服务端生成一次性的 code_verifier 和 state,然后将用户重定向到以下授权地址:
${GOODVISION_AUTHORIZATION_ENDPOINT}
?client_id=${GOODVISION_USER_CLIENT_ID}
&response_type=code
&scope=openid%20profile%20email
&redirect_uri=${GOODVISION_REDIRECT_URI}
&code_challenge=${GOODVISION_CODE_CHALLENGE}
&code_challenge_method=S256
&state=${GOODVISION_STATE}回调后由服务端交换授权码,检查 state、PKCE、签发方、目标 API 和有效期,再把登录状态放进 HttpOnly Session。浏览器只拿到 Session Cookie;用户身份没有 Client Secret。
七、接收 Webhook
Webhook Secret 按控制台显示的安全方式取得,不会出现在普通配置导出中。接收方至少要做到:
- 校验时间窗、原子登记 Nonce,再使用 HTTP 原始 Body 计算 HMAC-SHA256,采用常量时间比较。
- 明文或解密后都要校验并移除 Verification Token,再核对事件 Schema 和接入范围。
- 按
event_id幂等提交持久化 Inbox 后返回2xx;存储失败返回503,同一事件不能重复执行。 - 按资源版本处理乱序,发现缺口后使用授权状态接口回查;平台按响应结果决定投递重试。
完整 Header、Body、204 响应、加密流程和可运行签名脚本见接收 Webhook;取消、套餐变更和到期的完整 JSON 见订阅生命周期。
八、Node.js、Go、Python 示例
本节使用标准库演示不依赖 SDK 的直接 HTTP 调用。TypeScript/Node.js 项目也可以安装官方 @goodvision/sdk;Go 和 Python 当前使用本页的直接 HTTP 示例。生产代码还需要补充错误处理、超时和重试。
Node.js (内置 fetch)
const tokenResponse = await fetch(
process.env.GOODVISION_TOKEN_ENDPOINT,
{
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.GOODVISION_SERVICE_CLIENT_ID,
client_secret: process.env.GOODVISION_SERVICE_CLIENT_SECRET,
}),
},
);
const { access_token: accessToken } = await tokenResponse.json();
const response = await fetch(`${process.env.GOODVISION_API_BASE}/open-platform/v1/products`, {
headers: {
authorization: `Bearer ${accessToken}`,
"X-GV-Application-ID": process.env.GOODVISION_APPLICATION_ID,
},
});Go(标准库)
form := url.Values{
"grant_type": {"client_credentials"},
"client_id": {os.Getenv("GOODVISION_SERVICE_CLIENT_ID")},
"client_secret": {os.Getenv("GOODVISION_SERVICE_CLIENT_SECRET")},
}
req, _ := http.NewRequest("POST", os.Getenv("GOODVISION_TOKEN_ENDPOINT"), strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
// 解析 access_token 后,再调用当前 API Product 的接口。Python(标准库)
import os
import urllib.parse
import urllib.request
body = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": os.environ["GOODVISION_SERVICE_CLIENT_ID"],
"client_secret": os.environ["GOODVISION_SERVICE_CLIENT_SECRET"],
}).encode()
token_request = urllib.request.Request(
os.environ["GOODVISION_TOKEN_ENDPOINT"],
data=body,
headers={"content-type": "application/x-www-form-urlencoded"},
)
# 只在服务端读取 access_token,再调用当前 API Product 的接口。SDK 支持说明
官方 TypeScript/Node.js SDK 可通过 npm 安装并用于应用代码。Go、Python SDK 尚未提供;这些项目可继续使用本页的直接 HTTP 示例。无论使用 SDK 还是直接 HTTP,都不要把 Token、Secret、Cookie 或请求正文写入日志。
注意事项
- Token、Client Secret、Webhook Secret 只在服务端保存,不能写入前端、URL、日志或工单。
- 一个请求只能使用一种身份;用户身份和应用身份的 Client、Scope、Token 不能互换。
- 套餐、订阅、支付、账单和使用权限通过平台页面或已开通接口查询,SaaSdemo 不维护另一套结果。
- TypeScript/Node.js 项目可使用官方 SDK;其他语言使用 Developer Console 的配置、OpenAPI/事件契约和本页模板。
继续阅读:认证与授权、API 产品、SaaSdemo 接入案例。
登录后的用户与空间
完成身份结果核验后,服务端按用户与客户空间发送首次 {} 请求。平台负责默认空间和用户关联;示例中的业务范围不能固定为开发者企业,其他 API、事件和商业能力按需配置。