关于 salla-app-auth
面向应用开发者的 Salla OAuth 配置与商家令牌处理指导技能。它区分基于 Webhook 的 Easy Mode 和基于回调的 Custom Mode,并强调持久存储及安全轮换刷新令牌。
主要功能
- 01选择 OAuth 模式
- 02配置权限范围和重定向 URL
- 03接收或交换商家令牌
- 04按商家持久保存令牌
- 05跟踪访问令牌到期时间
- 06串行刷新以避免并发竞争
适用场景
- 01为已发布应用设置令牌交付
- 02在本地或 Postman 中测试 OAuth
- 03保存商家令牌以供后续 API 调用
- 04避免并发重复使用刷新令牌
MCP 相关
该技能使用 Salla Partners MCP 工具读取和设置 OAuth 权限范围,并连接应用配置。它指导开发者接收或交换令牌;令牌处理本身由运行时代码完成。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sallaapp/ salla-partners-agent-kit --skill salla-app-auth安装后在 Claude Code 中输入/salla-app-auth确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/app-events.mdcustom-mode.mdtoken-refresh.md详细介绍
Salla 应用认证流程
为应用获取并保留有效的商家令牌——选择 OAuth 模式、配置应用、接收/交换令牌,并安全地刷新令牌。请按顺序完成各步骤;完成每个关卡后再继续。步骤 2 会通过 Salla Partners MCP 执行操作;令牌处理则属于运行时代码。
要发布应用?→ 默认选择 Easy Mode。 令牌会通过
app.store.authorizewebhook 到达,因此你不需要 OAuth/callback或state处理。Custom Mode(/callback代码交换)主要用于本地开发/Postman;如果没有真实且合理的用例,在已发布的应用中采用这种模式可能会在 Salla 管理审核时被拒绝——因此请优先选择 Easy Mode,而不是习惯性地使用熟悉的 OAuth2 回调。
工具与 MCP
| 工具 | 操作 | 功能 |
|---|---|---|
salla_scopes | get / set | 读取或更新应用的 OAuth 作用域(slug、禁用标记、已选状态) |
salla_apps | connect | 在一次调用中设置作用域、重定向 URL 和 webhook_url(app.store.authorize 会自动发送到此地址) |
app.store.authorize是应用事件——会自动发送,无需调用订阅。 默认情况下,应用会订阅自身的应用事件,因此设置webhook_url后,令牌事件 (app.store.authorize)和其他所有app.*生命周期事件都会自动到达。你无需调用salla_events action=subscribe来订阅它——该操作仅用于商店事件 (order.*、product.*等)→ salla-webhooks。
文档:https://docs.salla.dev/421118m0.md · 应用事件:https://docs.salla.dev/421413m0.md · API 请求头:
Authorization: Bearer <access_token>。
步骤 0 — 了解情况
- 此应用要上架 App Store(→ Easy Mode),还是要在本地/Postman 中测试 (→ Custom Mode)?
- 令牌将存储在哪里? 将令牌持久化到真正的数据存储(数据库)中,以
merchantid 为键,并记录过期时间。/tmp和内存都不算存储——步骤 3 会说明原因。 - 你是否有令牌刷新并发处理方案?(你需要一个方案——见步骤 5)
步骤 1 — 选择 OAuth 模式
| 简易模式 ✅ | 自定义模式 | |
|---|---|---|
| 令牌的获取方式 | 通过 app.store.authorize webhook 负载 | 通过 /oauth/callback 进行代码交换 |
| 是否需要回调 URL? | 否 | 是 |
| 已发布的应用? | 推荐(默认) | 有合理的使用场景时允许 — 审核时可能会被拒绝 |
| 是否允许用于测试? | 是 | 是(Postman、本地开发) |
| 建议 | 默认 — 推荐所有应用使用 | 仅用于开发;需要有实际使用场景 |
| 令牌处理 | Salla 负责所有处理;你只需保存 | 由你实现完整的交换流程 |
决策规则 — 默认使用简易模式。 简易模式是所有应用推荐的默认模式——它更可靠、更直接,也是大多数使用场景中最容易实现的方式:Salla 通过 app.store.authorize webhook 交付令牌,因此无需构建、保护和维护回调或 state 流程。除非有具体的技术原因导致它无法使用,否则请使用此模式。自定义模式适用于开发期间的本地开发 / Postman;如果你确实需要在生产环境中使用,请准备好说明使用场景——发布的应用如果采用自定义模式却没有实际使用场景,审核时可能会被拒绝。
检查项:“已默认使用简易模式(或有真实且可供审核的自定义模式理由)?”
步骤 2 — 配置应用(Partners MCP)
使用 Partners MCP 设置 OAuth + webhook 配置,以传递令牌。
-
权限范围 — 使用
salla_scopes action=get和app_id读取 slug(以及每个应用的禁用标记);使用salla_scopes action=set更新它们(传入扁平的slug → "read" | "read_write" | ""映射),也可以在下方的 Connect 步骤中一并设置。
遵循最小权限原则: 仅授予应用实际使用的资源 slug;除非应用需要写入该资源,否则优先使用"read"而非"read_write"——不要对所有 slug 都默认设置宽泛的read_write权限。 -
连接 — 使用
salla_apps action=connect和app_id,并传入scopes({ "<slug>": "read" | "read_write" }— slug 和访问级别是分开的键,例如{"orders": "read_write"})。redirect_urls用于选择授权模式,而不只是注册 URL:- 简易模式 — 将
redirect_urls设置为应用的**easy_redirect_url**(Portal 会将其计算为 Salla 自有的回调 URL;使用salla_apps action=get读取)。生产环境中,此值为["https://accounts.salla.sa/callback/{app_id}"]。将 OAuthredirect_uri指向 Salla 自有的回调 URL,Salla 才会负责令牌交换。此模式需搭配webhook_url+webhook_security_strategy: "signature"。 - 自定义模式 — 将
redirect_urls设置为你自己的回调 URL。此处任何非 Salla URL 都会启用自定义模式——Salla 会将商家重定向到你的回调,并等待进行代码交换。因此,“不构建回调”(简易模式)和“在redirect_urls中填写什么”是同一个决定:简易模式下,应将redirect_urls指向 Salla 的回调,而不是你的应用。
(也在此处设置可信 IP — 参见下文“IP 白名单”部分。)
MCP 不会生成或轮换签名密钥——部署前请实时读取。
connect不会触碰 Webhook 签名密钥。请手动在 Partner Portal 中创建或轮换密钥,网址为https://portal.salla.partners/apps/{app_id}——提供包含应用真实 ID 的链接,例如https://portal.salla.partners/apps/1234567,而不是{app_id}占位符。在此处轮换密钥会使旧值失效。MCP 只能读取该密钥:每次部署前,请通过salla_apps action=get读取当前密钥(webhook_secret字段),并将该确切值设为验证环境变量。切勿使用从之前的会话或上下文压缩前记住的密钥——如果它已在 Portal 中轮换,就会失效,所有X-Salla-Signature检查都会失败,令牌也永远不会到达。 (签名验证本身 → salla-webhooks。)offline_access不应放入connect作用域映射中。 它是用于启用刷新令牌的 OAuth2 令牌作用域,只应放在授权 URL 中(以空格分隔,例如scope=offline_access orders.read_write)。connect映射只接受资源 slug(例如{"orders": "read_write"})。 - 简易模式 — 将
-
无需为令牌事件调用订阅。
app.store.authorize是应用事件——在步骤 2 中设置webhook_url后,该事件会自动发送,其他所有app.*生命周期事件也一样。(只有在应用需要响应商店事件时,才使用salla_events action=subscribe→ salla-webhooks。)
手动备用方案: Partners Portal → App Keys / Webhooks / App Scope。
检查项:“资源作用域已应用,并且 redirect_urls 与预期模式匹配——Easy Mode → redirect_urls = 应用的 easy_redirect_url(生产环境:["https://accounts.salla.sa/callback/{app_id}"]),且已设置 webhook_url 和 webhook_security_strategy(app.store.authorize 会自动发送到该 URL——无需调用订阅;如果没有 webhook_url,该事件就无处发送,令牌也永远不会到达);Custom Mode → 你自己的回调 URL?”
检查项:“部署前:通过 salla_apps action=get 读取当前 webhook_secret,并将该确切值设入验证环境——而不是使用从之前的会话中保留的密钥(它可能已轮换)?”
步骤 3 — 接收或交换令牌
简易模式(生产环境)
- 商家安装应用 → Salla 向你的 webhook 发送
app.store.authorize。 - 你的处理程序从
payload.data中读取access_token和refresh_token,并以merchant为键保存两者。 - 商家更新应用时,Salla 会先发送
app.updated,再发送app.store.authorize——同一个处理程序会收到新的令牌。
处理程序结构:先验证签名(salla-webhooks),
然后在收到 app.store.authorize 时,以 merchant 为键对 access_token / refresh_token /
expires * 1000 执行 upsert,并立即返回 200。完整的处理程序代码:
references/app-events.md。
请将令牌持久化到真正的数据存储中——
/tmp和内存都不是存储。 将 access/refresh 令牌写入以merchant为键的持久化数据库(Postgres、MySQL、DynamoDB、带持久化功能的 Redis, ……)。在无服务器环境/Vercel 上,文件系统(/tmp)和任何模块级 变量都是 临时的——每次冷启动都会清空:令牌会消失,下一次 API 调用会返回 403,商家看起来像是卸载了应用,尽管他们并没有。app.store.authorize在每次安装/更新时只会发送一次,因此令牌丢失后不会再次发送——只有重新安装 才能恢复。从第一次提交代码起就使用真正的存储。
密钥安全(两种模式都适用): access/refresh 令牌和客户端密钥都是 密钥——请加密静态存储,绝不要将它们写入日志、错误信息或 诊断信息中。重定向 URL 和 webhook URL 必须使用 HTTPS。将应用限制为已知的服务器 IP(IP 白名单,见下文)。
简易模式检查清单:已设置 webhook URL(步骤 2) · app.store.authorize 会自动发送到该 URL
(应用事件——无需调用订阅接口) · 载荷中授予的 data.scope 包含
offline_access(因此会签发刷新令牌) · 数据库按商家存储 access_token / refresh_token / token_expires_at ·
处理程序执行 upsert(而不是 insert)。
自定义模式(测试 / 本地开发)
授权码流程——授权请求(必须包含 offline_access,redirect_uri
必须与 Portal 中的完全一致)→ 回调(code + state)→ POST /oauth2/token 交换
→ 持久化两个令牌。此流程有几个 Salla 特有的回调陷阱(注册回调前先部署回调、由 Salla 发起的安装会发送它们自己的 state、state 中的连字符会被移除、code 只能使用一次、Next.js 在重定向中使用 cookie 的陷阱)。
完整流程(包括授权 URL、令牌交换、回调规则以及 PHP/Laravel 代码: 请阅读 references/custom-mode.md。
检查点:“是否已按商家持久化令牌,并根据 expires 推导出 tokenExpiresAt?”
步骤 4 — 了解令牌生命周期
| 令牌 | 有效期 | 说明 |
|---|---|---|
| 访问令牌 | 由 expires 字段决定(没有固定值) | app.store.authorize 载荷中的 expires 是 Unix 时间戳,也是唯一可信来源。不要假定有效期固定 |
| 刷新令牌 | 始终有效(永不过期) | 每次刷新只能使用一次——每次刷新都会返回新的刷新令牌;请保存它。令牌链本身不会过期 |
expires 是绝对的 Unix 时间戳(秒),而不是持续时间——请据此判断令牌是否过期,
绝不要使用硬编码的数值。存储前请先转换(来源:https://docs.salla.dev/421413m0.md):
// ✅ expires is an absolute Unix timestamp (seconds)
const expiresAt = new Date(payload.data.expires * 1000); // ms
// ❌ it is NOT a duration — never do: new Date(Date.now() + expires * 1000)
只有当作用域包含 offline_access 时,才会签发刷新令牌。始终包含
offline_access——如果不包含,就不会签发刷新令牌,因此 expires 到期后访问令牌无法
续期,商家必须重新安装应用。
检查点:“是否已存储两个令牌和转换后的 expiresAt,且作用域包含
offline_access?”
步骤 5 — 安全刷新令牌(危险区域)
每个刷新令牌只能使用一次:调用后会返回一个新的刷新令牌,并使旧令牌失效。 如果两次使用同一个刷新令牌(并行刷新竞争),Salla 的 OAuth 服务会将令牌链视为已泄露——它会撤销整条令牌链,商家必须重新安装应用,而且无法恢复。使用每个商家专属的锁串行执行刷新,确保刷新令牌在新令牌持久化之前不会离开锁的保护范围。
必需项:每个商家使用分布式互斥锁。 调用令牌端点之前,先获取该商家的专属锁。如果另一个进程已经持有该锁,则短暂等待,然后从数据库中重新读取已刷新的令牌,而不是重试刷新。使用经过验证的分布式锁库(例如用于 Redis 的 redlock,或数据库 advisory lock),由它负责处理持有者令牌和原子释放。要主动刷新(例如在 expiresAt 前约 1 天),不要等到收到 401 才刷新,并且在释放锁之前务必持久化新的访问令牌和刷新令牌——调用返回的瞬间,旧刷新令牌就已失效。
完整可运行代码(TS refreshTokenSafe + getValidToken,以及对应的 PHP 代码):
参阅 references/token-refresh.md。
检查项:“刷新是否受分布式锁保护、是否保存了两个新令牌,并且是否在过期前主动执行?”
步骤 6 — 获取并存储商家信息
获取令牌后(以及每次 app.store.authorize 后),刷新商家详情:
GET https://accounts.salla.sa/oauth2/user/info
Authorization: Bearer <access_token>
{
"id": 1771165749,
"name": "Test User",
"email": "[email protected]",
"merchant": {
"id": 1803665367,
"username": "dev-store-name",
"name": "My Store",
"plan": "special",
"status": "active",
"domain": "https://salla.sa/my-store"
}
}
商店 ID 是 merchant.id——位于响应的顶层,而不是 data 下。 user/info
没有 data 封装层(与 webhook/API 响应不同)。请稳妥地提取:
// ✅ user/info shape: { id, name, email, merchant: { id, ... } }
const merchantId = info?.merchant?.id; // number, top level — NOT info.data.merchant.id
if (!merchantId) throw new Error("user/info: missing merchant.id"); // guard BEFORE stringify
const storeId = String(merchantId);
// ❌ String(info?.merchant?.id ?? "") — turns a missing id into "" / a 0 id into "0", both
// truthy after String(), so the guard silently passes and a bad value reaches the DB
检查项:“商家 ID 和商店详情是否与令牌一同存储?”
危险信号
这些想法单独看似合理,却会破坏生产环境中的 Salla 应用。如果你发现自己有以下任何一种想法,请停下来重新阅读所指明的步骤。
| 容易产生的想法 | 为什么不对 |
|---|---|
“我懂 OAuth——直接搭建 /callback 流程就行。” | 这属于 Custom Mode。在没有合理使用场景的情况下,将其用于已发布的应用可能会在审核时被拒绝。默认使用 Easy Mode(步骤 1)。 |
| “按商家加刷新互斥锁太复杂了。” | 刷新令牌只能使用一次:并行重复使用会使整个令牌链失效,商家必须重新安装。这是不可妥协的要求(步骤 5)。 |
| “之后再加分布式锁/开发时先跳过。” | 开发时的习惯会带到生产环境,而且只有在真实并发下才会出现竞争问题——也就是说,问题会出现在生产环境中的真实商家那里。现在就一次性加上(步骤 5)。 |
| “刷新成功了——我只保存新的访问令牌。” | 必须保存两个新令牌。旧刷新令牌已经失效;丢弃新的刷新令牌,下次刷新就会失败(步骤 5)。 |
“offline_access 只是另一个资源范围。” | 它是 OAuth 令牌范围,只能放在授权 URL 中,不能放在 connect 映射中。省略它就不会签发刷新令牌(步骤 4)。 |
“expires 表示令牌有效多少秒。” | 它是绝对 Unix 时间戳。若将其当作持续时间,过期时间会被设到几十年后,而令牌会悄无声息地失效(步骤 4)。 |
“我先把令牌存到 /tmp(或模块变量)里。” | 在无服务器环境中,每次冷启动都会清空文件系统和内存——令牌会消失,下次调用就会返回 403,导致商家看起来像是尚未安装。从第一次提交开始,就将令牌持久化到以 merchant 为键的真实数据库中(步骤 3)。 |
“部署前我会重新运行 connect 来(重新)生成 webhook 密钥。” | connect 不会生成或轮换签名密钥——MCP 对该密钥只有读取权限。请在 Portal(https://portal.salla.partners/apps/{app_id})中创建/轮换密钥;部署前,通过 salla_apps action=get 读取当前的 webhook_secret,并使用该确切值(步骤 2)。 |
| “我会复用之前记下的 webhook 密钥。” | 在 Portal 中轮换密钥会使旧值失效,因此,记自之前会话或压缩前的密钥可能已经过期 → 所有验证都会失败。部署前,通过 salla_apps action=get 实时读取密钥(步骤 2)。 |
| “为了调试,把令牌写进日志没关系。” | 访问令牌/刷新令牌和客户端密钥都属于机密——静态加密,绝不记录到日志中(步骤 3)。 |
参考
OAuth 作用域
有两种不同的作用域上下文——请勿混淆:
1. salla_apps action=connect 作用域映射——仅资源作用域,slug 和级别作为
单独字段:
{ "orders": "read_write", "products": "read", "customers": "read_write" }
2. OAuth 授权 URL——以空格分隔的点分字符串。在此处包含 offline_access
(这是用于启用刷新令牌的 OAuth 令牌作用域,并非资源作用域):
scope=offline_access orders.read_write products.read customers.read_write
通过 salla_scopes action=get 确认应用有效的资源 slug(以及每个应用已禁用的标记):
orders products customers branches
settings webhooks payments taxes
specialoffers categories brands metadata
应用事件
| 事件 | 发生时机 | 操作 |
|---|---|---|
app.store.authorize | 应用安装或更新时(令牌刷新时不会触发——令牌刷新由你的 grant_type=refresh_token 调用完成) | 保存/更新两个令牌及其过期时间 |
app.installed | 首次安装 | 配置资源 |
app.uninstalled | 商家移除应用 | 清理商家数据并撤销已存储的令牌 |
完整载荷格式:references/app-events.md。生命周期处理 →
salla-app-lifecycle。
IP 白名单
将应用限制为已知服务器 IP:Partners Portal → My Apps → Your App → App
Trusted IPs(或在 salla_apps action=connect 中传入 trusted_ips)。降低生产应用的攻击面。
关键端点和库
| 用途 | URL |
|---|---|
| 授权 | https://accounts.salla.sa/oauth2/auth |
| 令牌交换和刷新 | https://accounts.salla.sa/oauth2/token |
| 用户信息 | https://accounts.salla.sa/oauth2/user/info |
| 直接安装 | https://s.salla.sa/apps/install/{app-id} |
| Salla API 基础 URL | https://api.salla.dev/admin/v2/ |
| 库 | 语言 | 仓库 |
|---|---|---|
salla/oauth2-merchant | PHP | https://github.com/SallaApp/oauth2-merchant |
@salla.sa/passport-strategy | JavaScript | https://github.com/SallaApp/passport-strategy |
| Laravel starter kit | PHP/Laravel | https://github.com/SallaApp/laravel-starter-kit |