Shopify 應用程式認證
Shopify 提供多種認證流程,取決於您的應用程式類型與使用情境。現代標準為 Token Exchange(2024 年後推出,適用於伺服器渲染應用程式)、Managed Installation(適用於無頭應用程式)以及 OAuth 2.0 授權碼授權(適用於舊版/自訂實作)。所有流程最終都會產生一個用於 Shopify GraphQL Admin 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,將授權碼交換為存取權杖(私密,僅伺服器端)
4. Shopify Admin API 授予權限範圍;權杖儲存在 session/資料庫中
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 包含:
// - 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 的 admin 輔助工具(推薦)
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 的 session 中介軟體自動重新整理。無需手動重新整理:
// 權杖會在每次請求時透明地重新整理
const response = await admin.graphql(query); // 內部處理重新整理
2. Managed Installation(無頭/自訂應用程式)
使用情境: 無頭商店前端、行動應用程式、第三方整合 流程: 商家授權應用程式 → Shopify 產生永久存取權杖(無密鑰輪替) 權杖生命週期: 長存;無需重新整理 安全性: 權杖為永久性;安全儲存在環境變數中
Managed Installation 設定:
在 Shopify 合作夥伴儀表板中:
- 應用程式設定 > API 憑證
- 在 Admin API 存取範圍下,選擇「Managed installation」
- 商家授予一次權限
- 將存取權杖複製到您的環境變數
# .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 授權碼授權(舊版,仍受支援)
使用情境: 採用傳統 OAuth 流程的公開應用程式 流程: 商家點擊「安裝」→ 應用程式重新導向至 Shopify OAuth → 商家授權 → 應用程式收到授權碼 → 應用程式將授權碼交換為權杖 權杖生命週期: 長存存取權杖(除非撤銷,否則不會過期) 安全性: 需要客戶端密鑰;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. 應用程式將授權碼交換為權杖(伺服器端,使用客戶端密鑰)
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');
// 將 state 儲存在 session 中(或資料庫)以供驗證
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 {
// 將授權碼交換為存取權杖
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;
// 儲存存取權杖(儲存在資料庫,而非 session,以確保持久性)
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);
Session 儲存介面卡
存取權杖必須持久儲存。Shopify 為常見的儲存後端提供了介面卡:
Prisma(適用於 Remix 的推薦方案)
Schema(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, // 注意:應用程式重新啟動時 Session 會遺失;僅限開發環境
});
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 管理儀表板)
- 限制:無法執行背景任務;使用者登出時受限
離線權杖:
- 範圍:應用程式已授予的權限範圍
- 到期時間:無;除非撤銷否則永久有效
- 使用情境:背景任務、Webhook、排程任務
- 限制:無;預設用於應用程式操作
在 Token Exchange 中請求離線權杖:
// shopify.app.ts
const shopify = shopifyApp({
// ...
auth: {
path: '/auth',
callbackPath: '/auth/callback',
},
});
// Remix 預設會自動請求離線權杖
// 無需動作;請使用 session.accessToken 進行 API 呼叫
將離線權杖用於 Webhook:
// 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 簽章驗證(類似 Webhook) 流程: 商店前端 → Liquid 代理請求 → 應用程式後端(驗證 HMAC)→ 回應
設定應用程式代理(shopify.app.toml):
[[extensions]]
type = "app_proxy"
name = "Storefront 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
// ...(因原文截斷,此為示意)
return json({ message: '成功' });
};
最佳實踐
- 始終使用 Token Exchange 用於新應用程式。它更安全,且無需管理客戶端密鑰。
- 對背景任務使用離線權杖。它們永久有效,直到被撤銷。
- 將權杖安全儲存在資料庫或安全環境變數中。永遠不要暴露給客戶端。
- 驗證所有回呼中的 HMAC 簽章,以防止 CSRF 和中間人攻擊。
- 使權杖範圍盡可能小,以限制資料庫暴露。
結論
Shopify 的認證流程旨在滿足從簡單安裝到複雜多層應用程式的各種用例。對於新開發,2024 年的 Token Exchange 流程是首選方法,其次是 Managed Installation 用於無頭應用程式。了解這些認證模式對於建置安全且可擴展的 Shopify 應用程式至關重要。