# OAuth 鉴权

> 公开 Shoplazza 应用获取 Access Token 的 OAuth 2.0 授权码流程：参数说明、关键步骤、令牌刷新与存储要求。

公开应用调用 Shoplazza Open API 之前必须先取得店铺授予的 **Access Token**。Shoplazza 采用 OAuth 2.0 的 Authorization Code 模式发放 Token。

**备注**

**本页定位**：参考层——把 OAuth 涉及的参数、流程、Token 生命周期一次性讲清。

**想跟着步骤把应用从零跑通**：去 [用 Node.js 与 Express 构建应用](/zh-CN/docs/app/getting-started/create-public-app/develop-app) 教程。

## 流程总览

```text
商家点击 Add App
        │
        ▼
① Shoplazza 后台回调你配置的 App URL（带 hmac + shop + store_id）
        │
        ▼
② 你的应用校验 hmac → 生成 state → 重定向到 Shoplazza 授权页
        │
        ▼
③ 商家在 Shoplazza 授权页同意你的 scope 请求
        │
        ▼
④ Shoplazza 回调你的 redirect_uri（带 code + state + hmac）
        │
        ▼
⑤ 你的应用校验 state、hmac → 用 code 换 access_token
        │
        ▼
⑥ 持久化 (shop, access_token, refresh_token, expires_at)
        │
        ▼
   调用 Open API，Header: `Access-Token: <access_token>`
```

下图为完整的 OAuth 授权交互流程：

![](https://cnres.appracle.com/14a9f36bfa021e984a1cb1f5561c318c.png)

## 关键参数

| 参数 | 来源 | 说明 |
| --- | --- | --- |
| `client_id` | 合作伙伴中心 | 应用唯一标识，公开值 |
| `client_secret` | 合作伙伴中心 | 应用密钥，**绝不可泄露给前端** |
| `redirect_uri` | 你配置的回调地址 | 必须与合作伙伴中心配置完全一致 |
| `scope` | 你声明的权限集合 | 逗号或空格分隔的 scope 列表 |
| `state` | 你生成的随机串 | 防 CSRF，回调时原样返回，必须校验 |
| `hmac` | Shoplazza 在 query 中带回 | 防请求伪造，必须校验，算法见 [HMAC 签名校验](/zh-CN/docs/app/building-blocks/authentication/signature-verification) |
| `code` | Shoplazza 回调返回 | Authorization Code，**只能用一次** |
| `access_token` | Token 接口返回 | 调 API 用的凭证，放在 `Access-Token` 请求头 |
| `refresh_token` | Token 接口返回 | 用于刷新 access_token |
| `expires_at` | Token 接口返回 | access_token 过期时间戳（秒） |

## 关键步骤

### ② 校验入参并跳转授权

商家从 Shoplazza 应用市场点击 **Add App** 后，Shoplazza 会请求你在合作伙伴中心配置的 App URL，URL 形如：

```text
https://your-app.example.com/auth/install?hmac=<hmac>&install_from=app_store&shop=<store_name>.myshoplaza.com&store_id=<store_id>
```

你的应用必须：

1.  校验 `hmac`（见 [HMAC 签名校验](/zh-CN/docs/app/building-blocks/authentication/signature-verification)）
2.  校验 `shop` 是否以 `.myshoplaza.com` 结尾
3.  生成随机 `state` 并持久化（用于回调时比对）
4.  重定向商家到 Shoplazza 授权页：

```text
https://<shop>/admin/oauth/authorize
  ?client_id=<client_id>
  &scope=<scopes>
  &redirect_uri=<redirect_uri>
  &response_type=code
  &state=<state>
```

商家在 Shoplazza 授权页会看到应用申请的权限（scope）确认界面：

![](https://cnres.appracle.com/9054b42e77708a60dd3a313fbf4f0fd2.png) ![](https://cnres.appracle.com/4ddd191ef4bd6e44c9ef970085880f92.png)

### ④ ⑤ 回调与换 Token

商家点击安装、在授权页确认后：

![](https://cnres.appracle.com/4b3e41fe257700dc9dbfc68415dc7a4b.png)

商家同意授权后，Shoplazza 回调你的 `redirect_uri`：

```text
https://your-app.example.com/auth/callback?code=<code>&shop=<shop>&state=<state>&hmac=<hmac>
```

校验顺序：

1.  校验 `hmac`
2.  校验 `state` 与 ② 生成的一致（防 CSRF）
3.  校验 `shop` 合法

通过后，向店铺 token 接口换 access_token：

```http
POST https://<shop>/admin/oauth/token
Content-Type: application/json

{
  "client_id": "<client_id>",
  "client_secret": "<client_secret>",
  "code": "<code>",
  "grant_type": "authorization_code",
  "redirect_uri": "<redirect_uri>"
}
```

成功响应：

```json
{
  "token_type": "Bearer",
  "expires_at": 1550546245,
  "access_token": "<access_token>",
  "refresh_token": "<refresh_token>",
  "store_id": "2",
  "store_name": "xiong1889"
}
```

**字段说明：**

*   `expires_at`：access_token 过期时间戳（秒）
*   `refresh_token`：用于刷新 access_token
*   `store_id` / `store_name`：店铺标识

也可使用 Shoplazza OAuth SDK（Go）换取 token：

```go
import (
  co "github.com/shoplazza-os/oauth-sdk-go"
  "github.com/shoplazza-os/oauth-sdk-go/shoplazza"
)

oauth := &co.Config{
                ClientID:     "s1Ip1WxpoEAHtPPzGiP2rK2Az-P07Nie7V97hRKigl4",
                ClientSecret: "{YOUR_CLIENT_SECRET}",
                Endpoint:     shoplazza.Endpoint,
                RedirectURI:  "https://3830-43-230-206-233.ngrok.io/oauth_sdk/redirect_uri/",
                Scopes:       []string{"read_shop"},
        }
token, err := oauth.Exchange(context.Background(),"xxx.myshoplaza.com", "code"))
```

**Token 有效期**

`access_token` 与 `refresh_token` 的有效期均为 **1 年**。请在 `access_token` 接近 `expires_at` 前用 `refresh_token` 换取新 token（见下方「刷新 Access Token」）。

### ⑥ 持久化与调用

把 `(shop, access_token, refresh_token, expires_at)` 写入存储（数据库、Redis 等）。调用 Open API 时在 Header 携带：

```http
GET https://<shop>/openapi/2022-01/customers HTTP/1.1
Access-Token: <access_token>
```

## 刷新 Access Token

当 access_token 接近 `expires_at` 时，用 refresh_token 换新的：

```http
POST https://<shop>/admin/oauth/token
Content-Type: application/json

{
  "client_id": "<client_id>",
  "client_secret": "<client_secret>",
  "refresh_token": "<refresh_token>",
  "grant_type": "refresh_token",
  "redirect_uri": "<redirect_uri>"
}
```

响应字段与初次换 token 相同。

也可使用 Shoplazza OAuth SDK（Go）刷新 token：

```go
import (
  co "github.com/shoplazza-os/oauth-sdk-go"
  "github.com/shoplazza-os/oauth-sdk-go/shoplazza"
)

oauth := &co.Config{
                ClientID:     "s1Ip1WxpoEAHtPPzGiP2rK2Az-P07Nie7V97hRKigl4",
                ClientSecret: "{YOUR_CLIENT_SECRET}",
                Endpoint:     shoplazza.Endpoint,
                RedirectURI:  "https://3830-43-230-206-233.ngrok.io/oauth_sdk/redirect_uri/",
                Scopes:       []string{"read_shop"},
        }
token, err := oauth.RefreshToken(context.Background(), "xxx.myshoplaza.com", "refresh token")
```

## 安全要求

*   `client_secret` 与 `access_token` 一律保存在服务端，绝不下发到浏览器
*   每次回调必须校验 `hmac` 和 `state`
*   `code` 一次性，重复使用会失败
*   必须使用 HTTPS
*   **SSRF 防护**：校验 `shop` 参数是合法的 `*.myshoplaza.com` 域名
*   **CSRF 防护**：发起授权时生成随机 `state`，回调时校验并消费
*   **时序攻击防护**：比对 HMAC 使用 `crypto.timingSafeEqual`

## Token 存储约定

示例用 SQLite 以店铺域名为主键存储 `access_token`（表 `shop_tokens`：`shop_domain` 主键 / `access_token` / `updated_time`），店铺再次访问时直接读库、无需重新授权。生产环境建议：

*   改用 PostgreSQL / MySQL 等独立数据库
*   数据库路径通过环境变量（如 `DB_PATH`）配置，避免相对路径在不同部署环境出问题
*   用 `require("sqlite3")` 而非 `.verbose()`，避免额外调试日志影响性能

## 最佳实践

### 环境变量管理

*   **开发环境**：使用 `.env` 文件
*   **生产环境**：使用云平台的环境变量功能或密钥管理服务（如 AWS Secrets Manager、Azure Key Vault、阿里云 KMS 等）
*   永远不要将敏感信息硬编码到项目中
*   将 `.env` 添加到 `.gitignore`

```javascript
const CLIENT_SECRET = process.env.CLIENT_SECRET;
```

### HMAC 验证

所有来自 Shoplazza 的请求都应该验证 HMAC 签名：

```javascript
app.get("/auth", hmacValidator, (req, res) => {
  // 只有验证通过的请求才会执行这里的代码
});
```

### 项目工程化

生产项目建议采用前后端分离的工程化结构：

```text
shoplazza-app-demo/
├── server/
│   ├── routes/
│   ├── utils/
│   ├── database/
│   └── index.js
├── src/
│   ├── components/
│   ├── pages/
│   └── main.js
├── dist/
├── package.json
└── .env
```

## 参考实现

*   完整 Node.js + Express 实战代码：[用 Node.js 与 Express 构建应用](/zh-CN/docs/app/getting-started/create-public-app/develop-app)
*   各语言 SDK：[API 客户端库](/zh-CN/docs/app/developer-tools/api-libraries)
*   申请的 scope 列表：[访问权限（Scopes）](/zh-CN/docs/app/building-blocks/authentication/access-scopes)
