Community写作与编辑github.com

khadinakbarlabs/shopify-app-builder

开源 Shopify 应用开发插件,为 Claude Code、Codex、Cursor、OpenCode、Gemini CLI 及其他 AI 编程代理提供 32 项技能。

shopify-app-builder 是什么?

这是一个面向 AI 编程代理的开源 Shopify 应用开发插件,拥有 32 项技能,可帮助开发者快速构建 Shopify 应用。它支持多种 AI 平台,包括 Claude Code、Codex、Cursor、OpenCode 和 Gemini CLI,并专注于 Shopify 的核心认证流程。该插件提供了从现代 Token Exchange 到传统 OAuth 2.0 的多种认证方式详解,并附带了适用于 Remix、Express.js 等框架的完整代码示例和配置指南。其最核心的价值在于,它让 AI 代理能够通过自然语言交互直接执行 Shopify 应用开发中的认证设置、会话存储、API 调用等复杂任务,大幅降低了开发者的学习成本和重复劳动。通过使用此插件,开发者可以快速搭建安全、标准化的 Shopify 应用后端,专注于业务逻辑而非基础认证。

兼容平台✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI✓OpenCode
npx skills add khadinakbarlabs/shopify-app-builder

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Shopify 应用认证

Shopify 提供了多种认证流程,具体取决于您的应用类型和用例。现代标准是用于服务端渲染应用的 Token Exchange(令牌交换,2024+)、用于无头应用的 Managed Installation(托管安装)、以及用于传统/自定义实现的 OAuth 2.0 授权码授权模式。所有流程最终都会生成一个用于 Shopify GraphQL 管理 API 的访问令牌。

认证流程概览

1. Token Exchange(令牌交换,推荐用于 2024+)

适用场景: 服务端渲染应用(Remix、带 SSR 的 Next.js)、Shopify CLI 应用 流程: 商家安装应用 → Shopify 生成临时交换令牌 → 应用交换为访问令牌 安全性: 不暴露客户端密钥;每次请求使用 PKCE 风格轮换 令牌生命周期: 访问令牌是短期的;刷新令牌自动轮换

Token Exchange 图示:

1. 商家在 Shopify 管理后台点击“安装”
2. Shopify 重定向:https://your-app.com/auth/callback?code=EXCHANGE_TOKEN
3. 应用验证 HMAC,用 code 交换访问令牌(私密,仅在服务端进行)
4. Shopify 管理 API 授予权限;令牌存储在会话/数据库中
5. 令牌在下次请求时如果过期会自动刷新

Remix 实现(Token Exchange):

// shopify.app.ts(应用配置)
import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { restResources } from '@shopify/shopify-api/rest/admin/2026-07';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY || '',
  apiSecret: process.env.SHOPIFY_API_SECRET || '',
  scopes: process.env.SCOPES?.split(',') || [
    'write_products',
    'read_products',
    'write_orders',
    'read_orders',
    'write_customers',
    'read_customers',
    'write_discounts',
    'read_discounts',
    'write_fulfillments',
    'read_fulfillments',
    'write_inventory',
    'read_inventory',
  ],
  appUrl: process.env.SHOPIFY_APP_URL || 'http://localhost:3000',
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
  webhooks: {
    path: '/webhooks',
  },
  isEmbeddedApp: true, // Polaris 管理后台
  sessionStorage: new PrismaSessionStorage(prisma),
  restResources, // 包含 REST API 辅助工具
});

export default shopify;

环境变量(.env):

SHOPIFY_API_KEY=your-public-api-key-from-partner-dashboard
SHOPIFY_API_SECRET=your-private-api-secret
SHOPIFY_APP_URL=https://your-domain.ngrok.io  # 或生产 URL
SCOPES=write_products,read_products,write_orders,read_orders

认证路由(routes/auth.$.tsx - 全匹配路由):

import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { authenticate } = await import('~/shopify.server');
  return authenticate.admin(request); // Token Exchange 在这里进行
};

回调处理(routes/auth.callback.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  // 会话包含:
  // - session.accessToken(用于 API 调用的有效令牌)
  // - session.shop(商家的商店域名)
  // - session.scope(已授予的权限范围)
  // - session.state(可选的自定义数据)

  return redirect('/app'); // 认证后重定向到仪表盘
};

在 API 调用中使用令牌(routes/app.products.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { GraphQLClient } from 'graphql-request';

export const loader = async ({ request }) => {
  const { session, admin } = await authenticate.admin(request);

  // 方法 1:使用 Shopify 的管理辅助工具(推荐)
  const response = await admin.graphql(`
    query GetProducts {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
          }
        }
      }
    }
  `);

  // 方法 2:手动 GraphQL 调用
  const client = new GraphQLClient(
    `https://${session.shop}/admin/api/2026-07/graphql.json`,
    {
      headers: {
        'X-Shopify-Access-Token': session.accessToken,
        'Content-Type': 'application/json',
      },
    }
  );

  const data = await client.request(/* ... */);

  return json({ products: response.data?.products?.edges || [] });
};

令牌刷新(自动): Token Exchange 的令牌会通过 Shopify 的会话中间件自动刷新。无需手动刷新:

// 令牌会在每次请求时透明地刷新
const response = await admin.graphql(query); // 内部处理刷新

2. Managed Installation(托管安装,用于无头/自定义应用)

适用场景: 无头商店、移动应用、第三方集成 流程: 商家授权应用 → Shopify 生成永久访问令牌(无需密钥轮换) 令牌生命周期: 长期有效;无需刷新 安全性: 令牌是永久的;安全地存储在环境变量中

Managed Installation 设置:

在 Shopify 合作伙伴仪表盘:

  1. 应用设置 > API 凭证
  2. 在“管理 API 访问范围”下选择“托管安装”
  3. 商家授予一次权限
  4. 将访问令牌复制到您的环境变量中
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=example-shop.myshopify.com

使用 Managed Installation 令牌:

import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient(
  `https://${process.env.SHOPIFY_SHOP_URL}/admin/api/2026-07/graphql.json`,
  {
    headers: {
      'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '',
    },
  }
);

// 令牌永不过期;可随时调用 API
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);

3. OAuth 2.0 Authorization Code Grant(授权码授权模式,传统但仍受支持)

适用场景: 使用传统 OAuth 流程的公共应用 流程: 商家点击“安装” → 应用重定向到 Shopify OAuth → 商家授权 → 应用接收 code → 应用用 code 交换令牌 令牌生命周期: 长期有效的访问令牌(除非被撤销,否则不会过期) 安全性: 需要客户端密钥;PKCE 可选

OAuth 流程图示:

1. 商家访问:https://your-app.com/auth
2. 应用重定向到:https://your-shop.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://your-app.com/auth/callback&state=RANDOM
3. 商家在 Shopify 管理后台授权应用
4. Shopify 重定向回:https://your-app.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=your-shop.myshopify.com
5. 应用验证 HMAC 和 state
6. 应用用 code 交换令牌(服务端,使用客户端密钥)
7. 应用将令牌存储在数据库中

Express.js OAuth 示例:

import express from 'express';
import axios from 'axios';
import crypto from 'crypto';

const app = express();

const API_KEY = process.env.SHOPIFY_API_KEY || '';
const API_SECRET = process.env.SHOPIFY_API_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'https://your-app.com/auth/callback';
const SCOPES = 'write_products,read_products';

// 第 1 步:将商家重定向到 Shopify OAuth
app.get('/auth', (req, res) => {
  const shop = req.query.shop as string;

  if (!shop || !shop.includes('.myshopify.com')) {
    return res.status(400).send('缺少或无效的 shop 参数');
  }

  const state = crypto.randomBytes(16).toString('hex');
  const nonce = crypto.randomBytes(16).toString('hex');

  // 将会话状态存储在会话(或数据库)中以供验证
  req.session.state = state;
  req.session.nonce = nonce;

  const authUrl = new URL(
    `/admin/oauth/authorize`,
    `https://${shop}`
  );
  authUrl.searchParams.append('client_id', API_KEY);
  authUrl.searchParams.append('scope', SCOPES);
  authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.append('state', state);

  res.redirect(authUrl.toString());
});

// 第 2 步:处理 OAuth 回调
app.get('/auth/callback', async (req, res) => {
  const { code, hmac, shop, state } = req.query;

  // 验证 HMAC
  const message = Object.entries(req.query)
    .filter(([key]) => key !== 'hmac')
    .map(([key, value]) => `${key}=${value}`)
    .sort()
    .join('&');

  const hash = crypto
    .createHmac('sha256', API_SECRET)
    .update(message, 'utf8')
    .digest('base64');

  if (hash !== hmac) {
    return res.status(401).send('检测到未经授权的请求');
  }

  // 验证 state
  if (state !== req.session.state) {
    return res.status(401).send('State 不匹配');
  }

  try {
    // 用 code 交换访问令牌
    const response = await axios.post(
      `https://${shop}/admin/oauth/access_token`,
      {
        client_id: API_KEY,
        client_secret: API_SECRET,
        code,
      }
    );

    const { access_token, scope } = response.data;

    // 存储访问令牌(存储在数据库中,而非会话中,以实现持久化)
    await storeAccessToken(shop as string, access_token, scope);

    // 重定向到应用仪表盘
    res.redirect(`/app?shop=${shop}`);
  } catch (error) {
    console.error('令牌交换错误:', error);
    res.status(500).send('认证失败');
  }
});

// 存储令牌的辅助函数
async function storeAccessToken(shop: string, token: string, scope: string) {
  // 存储在数据库中(使用模拟存储的示例)
  const db = {
    shops: {} as Record<string, { token: string; scope: string }>,
  };
  db.shops[shop] = { token, scope };
}

app.listen(3000);

会话存储适配器

访问令牌必须持久存储。Shopify 为常见存储后端提供了适配器:

Prisma(推荐用于 Remix)

模式定义(prisma/schema.prisma):

model Session {
  id        String    @id
  shop      String
  state     String
  isOnline  Boolean   @default(false)
  accessToken String
  refreshToken String?
  scope     String
  expiresAt DateTime?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@unique([shop, state])
  @@index([shop])
}

设置(shopify.app.ts):

import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  // ...
  sessionStorage: new PrismaSessionStorage(prisma),
});

Redis

import { RedisSessionStorage } from '@shopify/shopify-app-session-storage-redis';
import redis from 'redis';

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
});

const sessionStorage = new RedisSessionStorage({
  client: redisClient,
  prefix: 'shopify_session:',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

内存存储(仅限开发环境)

import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';

const sessionStorage = new MemorySessionStorage();

const shopify = shopifyApp({
  // ...
  sessionStorage, // 注意:应用重启后会话会丢失;仅限开发环境
});

DynamoDB

import { DynamoDBSessionStorage } from '@shopify/shopify-app-session-storage-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const dynamoDBClient = new DynamoDBClient({ region: 'us-east-1' });

const sessionStorage = new DynamoDBSessionStorage({
  client: dynamoDBClient,
  tableName: 'shopify-sessions',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

在线令牌 vs. 离线令牌

在线令牌:

  • 范围:当前用户的权限(通常是管理员用户)
  • 过期时间:24 小时
  • 用例:基于浏览器的操作(Polaris 管理仪表盘)
  • 限制:不能运行后台任务;用户注销后限制使用

离线令牌:

  • 范围:应用已授予的权限范围
  • 过期时间:无;永久有效,除非被撤销
  • 用例:后台任务、Webhooks、定时任务
  • 限制:无;默认用于应用操作

在 Token Exchange 中请求离线令牌:

// shopify.app.ts
const shopify = shopifyApp({
  // ...
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
});

// Remix 默认自动请求离线令牌
// 无需额外操作;使用 session.accessToken 进行 API 调用

在 Webhooks 中使用离线令牌:

// webhooks/products-update.ts
export const webhooks = {
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  PRODUCTS_UPDATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-update',
  },
};

export default defineWebhooksConfig(
  async (request, { admin, session }) => {
    const { body, query } = await graphql.query(request, {
      query: GET_PRODUCT,
      variables: { id: 'gid://shopify/Product/123' },
    });

    // session.accessToken 是离线令牌;在此处有效
    console.log(`处理了商店 ${session.shop} 的 Webhook,使用了令牌`);
  },
  webhooksConfig
);

应用代理认证

适用场景: 商店前端(面向公众)对应用后端的请求 认证方式: HMAC 签名验证(类似于 Webhooks) 流程: 商店前端 → Liquid 代理请求 → 应用后端(验证 HMAC)→ 响应

设置应用代理(shopify.app.toml):

[[extensions]]
type = "app_proxy"
name = "商店 API"
url = "/api/proxy"
subpath = "loyalty"  # 对 /apps/loyalty/* 的请求被路由到这里

应用代理处理器(Remix 路由 routes/api/proxy.ts):

import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';

export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const hmac = url.searchParams.get('hmac') || '';
  const timestamp = url.searchParams.get('_t') || '';
  const shop = url.searchParams.get('shop') || '';

  // 验证 HMAC(类似于 OAuth 回调)
  const sortedParams = [...url.searchParams.entries()]
    .filter(([key]) => key !== 'hmac')
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, val]) => `${key}=${val}`)
    .join('');

  const generatedHmac = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
    .update(sortedParams, 'utf8')
    .digest('hex');

  if (hmac !== generatedHmac) {
    return json({ error: 'HMAC 验证失败' }, { status: 401 });
  }

  // 处理代理请求(例如,返回产品评级)
  return json({
    shop,
    data: { /* 您的自定义数据 */ }
  });
};

最佳实践

  1. 优先使用 Token Exchange:对于新的 Remix/Next.js 应用,Token Exchange 是最现代、最安全的方法。它消除了客户端密钥暴露的风险,并提供了自动令牌刷新功能。
  2. 为后端任务使用离线令牌:Webhooks 和后台作业需要离线令牌,该令牌在商家注销后仍然有效。
  3. 安全存储令牌:务必将访问令牌存储在持久化数据库(如 Prisma、Redis、DynamoDB)中,切勿仅存储在会话中。
  4. 验证所有请求:对于 OAuth 回调,始终验证 HMAC 和 state 参数,以防止 CSRF 攻击。
  5. 配置适当的权限范围:仅请求您的应用实际需要的权限。不必要的权限可能会引起商家的警觉,并可能导致审核问题。
  6. 使用 Shopify CLI 创建并管理任务:Shopify CLI 提供了用于创建、测试和部署应用的基本工作流程。
  7. 遵循 Shopify 的扩展性指南:在设计 Webhooks 和后台作业时,请遵循 Shopify 的速率限制和重试策略。
  8. 在开发环境中使用 ngrok:SHOPIFY_APP_URL 应指向一个可公开访问的 URL,以用于本地开发。npx ngrok http 3000 通常用于此目的。
  9. 启用应用代理以实现公共 API:对于嵌入到 Liquid 主题中的应用,应用代理是一种有状态请求的有效方法,无需完整的 OAuth 流程。
  10. 监控令牌使用情况:在开发过程中,使用日志记录和监控来跟踪令牌交换、刷新和错误。Shopify 伙伴仪表盘提供了广泛的 API 使用情况分析。

相关技能