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