mcpField
S
salla-app-auth

salla-app-auth

github.com/sallaapp安全290 次安装2 周前更新
代码仓库

关于 salla-app-auth

面向应用开发者的 Salla OAuth 配置与商家令牌处理指导技能。它区分基于 Webhook 的 Easy Mode 和基于回调的 Custom Mode,并强调持久存储及安全轮换刷新令牌。

主要功能

  1. 01选择 OAuth 模式
  2. 02配置权限范围和重定向 URL
  3. 03接收或交换商家令牌
  4. 04按商家持久保存令牌
  5. 05跟踪访问令牌到期时间
  6. 06串行刷新以避免并发竞争

适用场景

  1. 01为已发布应用设置令牌交付
  2. 02在本地或 Postman 中测试 OAuth
  3. 03保存商家令牌以供后续 API 调用
  4. 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.md
custom-mode.md
token-refresh.md

详细介绍

Salla 应用认证流程

为应用获取并保留有效的商家令牌——选择 OAuth 模式、配置应用、接收/交换令牌,并安全地刷新令牌。请按顺序完成各步骤;完成每个关卡后再继续。步骤 2 会通过 Salla Partners MCP 执行操作;令牌处理则属于运行时代码。

要发布应用?→ 默认选择 Easy Mode。 令牌会通过 app.store.authorize webhook 到达,因此你不需要 OAuth /callback 或 state 处理。Custom Mode(/callback 代码交换)主要用于本地开发/Postman;如果没有真实且合理的用例,在已发布的应用中采用这种模式可能会在 Salla 管理审核时被拒绝——因此请优先选择 Easy Mode,而不是习惯性地使用熟悉的 OAuth2 回调。

工具与 MCP

工具操作功能
salla_scopesget / set读取或更新应用的 OAuth 作用域(slug、禁用标记、已选状态)
salla_appsconnect在一次调用中设置作用域、重定向 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 — 了解情况

  1. 此应用要上架 App Store(→ Easy Mode),还是要在本地/Postman 中测试 (→ Custom Mode)?
  2. 令牌将存储在哪里? 将令牌持久化到真正的数据存储(数据库)中,以 merchant id 为键,并记录过期时间。/tmp 和内存都不算存储——步骤 3 会说明原因。
  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 配置,以传递令牌。

  1. 权限范围 — 使用 salla_scopes action=get 和 app_id 读取 slug(以及每个应用的禁用标记);使用 salla_scopes action=set 更新它们(传入扁平的 slug → "read" | "read_write" | "" 映射),也可以在下方的 Connect 步骤中一并设置。
    遵循最小权限原则: 仅授予应用实际使用的资源 slug;除非应用需要写入该资源,否则优先使用 "read" 而非 "read_write"——不要对所有 slug 都默认设置宽泛的 read_write 权限。

  2. 连接 — 使用 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}"]。将 OAuth redirect_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"})。

  3. 无需为令牌事件调用订阅。 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 — 接收或交换令牌

简易模式(生产环境)
  1. 商家安装应用 → Salla 向你的 webhook 发送 app.store.authorize。
  2. 你的处理程序从 payload.data 中读取 access_token 和 refresh_token,并以 merchant 为键保存两者。
  3. 商家更新应用时,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 基础 URLhttps://api.salla.dev/admin/v2/
库语言仓库
salla/oauth2-merchantPHPhttps://github.com/SallaApp/oauth2-merchant
@salla.sa/passport-strategyJavaScripthttps://github.com/SallaApp/passport-strategy
Laravel starter kitPHP/Laravelhttps://github.com/SallaApp/laravel-starter-kit

评论

支持 Markdown