<!-- CMS 标题:OAuth 2.0 授权与 Access Token 获取 -->

<!-- 描述说明:介绍外部应用如何引导站斧主账号授权、接收 code,并通过 PHP、Java、Python、Go 服务端换取 access_token。 -->

<!-- 使用方式:点击富文本编辑器工具栏的 </> 源码按钮,粘贴本文件全部内容,再切回可视化模式。 -->

<div style="box-sizing:border-box;width:100%;color:#24292f;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',Arial,sans-serif;font-size:15px;line-height:1.75;word-break:break-word;">

<p style="margin:10px 0;line-height:1.8;">本文面向通过服务端 HTTP 接入站斧开放平台的外部开发者,说明如何引导用户授权、接收授权码 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code>,以及通过授权码换取 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_token</code>。</p>

<blockquote style="margin:16px 0;padding:10px 16px;border-left:4px solid #2f81f7;background:#f0f6ff;color:#334155;">

<p style="margin:10px 0;line-height:1.8;">安全要求:<code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code> 只能保存在服务端,不得写入网页、浏览器插件、移动端安装包、公开仓库或客户端日志。</p>

</blockquote>

<h2 id="section" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">1. 接入流程</h2>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">外部应用服务端                         用户浏览器                      站斧开放平台

     |                                  |                                |

     | 生成授权页面地址                  |                                |

     |--------------------------------->|                                |

     |                                  | 打开授权页并登录主账号          |

     |                                  |------------------------------->|

     |                                  |<-------------------------------|

     |<--------- 回调地址?code=... -----|                                |

     |                                                                   |

     | POST rux.open.auth.token.create(带签名)                          |

     |------------------------------------------------------------------->|

     |<---------------- access_token / refresh_token ---------------------|

</code></pre>

<p style="margin:10px 0;line-height:1.8;">完整流程如下:</p>

<ol style="margin:10px 0;padding-left:26px;">

<li style="margin:5px 0;line-height:1.75;">在开放平台配置应用的回调地址,并取得 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_key</code> 和 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code>。</li>

<li style="margin:5px 0;line-height:1.75;">服务端生成授权页面地址,引导用户使用站斧主账号完成登录和授权。</li>

<li style="margin:5px 0;line-height:1.75;">站斧开放平台将浏览器重定向到应用回调地址,并附加 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code>。</li>

<li style="margin:5px 0;line-height:1.75;">应用服务端校验回调请求,读取 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code>。</li>

<li style="margin:5px 0;line-height:1.75;">服务端调用 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">rux.open.auth.token.create</code>,用 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code> 换取令牌。</li>

<li style="margin:5px 0;line-height:1.75;">服务端安全保存令牌,后续调用受保护 API 时携带 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_token</code>。</li>

</ol>

<h2 id="section-1" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">2. 环境地址</h2>

<table style="width:100%;margin:14px 0;border-collapse:collapse;border-spacing:0;font-size:14px;line-height:1.6;">

<thead>

<tr>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">用途</th>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">正式环境地址</th>

</tr>

</thead>

<tbody>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">用户授权页</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">https://mms.zhanfubrowser.com/#/open/auth</code></td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">开放 API 网关</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">https://gw-open.zhanfubrowser.com/api/route</code></td>

</tr>

</tbody>

</table>

<p style="margin:10px 0;line-height:1.8;">开放 API 网关只接受 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">POST</code> 请求。</p>

<h2 id="section-2" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">3. 字段从哪里获得</h2>

<table style="width:100%;margin:14px 0;border-collapse:collapse;border-spacing:0;font-size:14px;line-height:1.6;">

<thead>

<tr>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">字段</th>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">来源</th>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">是否敏感</th>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">说明</th>

</tr>

</thead>

<tbody>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">response_type</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">应用固定填写</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">否</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">当前固定为 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code></td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_key</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">站斧开放平台为应用分配</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">可公开标识</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">用于标识调用方应用</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">站斧开放平台为应用分配</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><strong>是</strong></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">仅在服务端生成签名,不随请求发送</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">回调地址</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">开发者在开放平台配置</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">否</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">必须是应用可接收 HTTP 回调的地址</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">用户授权后,由站斧重定向到回调地址时返回</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><strong>是</strong></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">有效期为 10 分钟,应尽快换取令牌</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">timestamp</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">调用方服务端生成</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">否</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">当前 Unix 时间戳,单位为秒,例如 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">1603293524</code></td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">api</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">接口文档规定</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">否</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">换令牌时固定为 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">rux.open.auth.token.create</code></td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">sign</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">调用方服务端计算</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><strong>是</strong></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">按本文第 6 节的规则生成</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_token</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">换令牌接口返回</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><strong>是</strong></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">调用用户授权类 API 时使用</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">refresh_token</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">换令牌接口返回</td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><strong>是</strong></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">用途与刷新方式以平台对应接口文档为准</td>

</tr>

</tbody>

</table>

<h2 id="section-3" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">4. 第一步:生成用户授权地址</h2>

<p style="margin:10px 0;line-height:1.8;">授权地址格式:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">https://mms.zhanfubrowser.com/#/open/auth?response_type=code&app_key={APP_KEY}

</code></pre>

<p style="margin:10px 0;line-height:1.8;">示例:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">https://mms.zhanfubrowser.com/#/open/auth?response_type=code&app_key=your_app_key

</code></pre>

<p style="margin:10px 0;line-height:1.8;">生成地址时,每个查询参数值都应进行 URL 编码。用户必须使用主账号授权;应用允许发起重复授权。</p>

<blockquote style="margin:16px 0;padding:10px 16px;border-left:4px solid #2f81f7;background:#f0f6ff;color:#334155;">

<p style="margin:10px 0;line-height:1.8;">若开放平台支持 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">state</code> 参数,建议使用不可预测且一次性的 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">state</code> 关联授权发起请求和回调,以防止跨站请求伪造。是否支持以及是否原样返回,应以平台实际行为为准。</p>

</blockquote>

<h2 id="section-4" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">5. 第二步:接收授权回调</h2>

<p style="margin:10px 0;line-height:1.8;">授权成功后,浏览器会跳转到应用在开放平台配置的回调地址:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">https://your.example.com/oauth/callback?code=74afa4f59c4048d3aba7a152089e9db87797d6cc

</code></pre>

<p style="margin:10px 0;line-height:1.8;">服务端应执行以下操作:</p>

<ol style="margin:10px 0;padding-left:26px;">

<li style="margin:5px 0;line-height:1.75;">仅允许 HTTPS 回调地址。</li>

<li style="margin:5px 0;line-height:1.75;">读取并校验 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code> 非空。</li>

<li style="margin:5px 0;line-height:1.75;">不在页面、访问日志或错误日志中输出完整 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code>。</li>

<li style="margin:5px 0;line-height:1.75;">在 10 分钟内调用换令牌接口。</li>

<li style="margin:5px 0;line-height:1.75;">即使平台允许同一 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code> 重复换取相同令牌,应用也应只处理一次回调并复用已保存的结果。</li>

</ol>

<h2 id="section-5" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">6. 第三步:生成换令牌签名</h2>

<p style="margin:10px 0;line-height:1.8;">换令牌请求参与签名的参数如下,<strong>不包含 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">sign</code> 本身,也不包含 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code> 参数</strong>:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">api=rux.open.auth.token.create

app_key=your_app_key

code=your_code

timestamp=1603293524

</code></pre>

<p style="margin:10px 0;line-height:1.8;">签名步骤:</p>

<ol style="margin:10px 0;padding-left:26px;">

<li style="margin:5px 0;line-height:1.75;">删除键名为空、值为 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">null</code> 或空字符串的参数;数值 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">0</code> 不应删除。</li>

<li style="margin:5px 0;line-height:1.75;">按参数名的字符升序排列。</li>

<li style="margin:5px 0;line-height:1.75;">按“参数名 + 参数值”直接连接,不添加 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">=</code>、<code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">&</code> 或空格。</li>

<li style="margin:5px 0;line-height:1.75;">在连接结果的首尾分别添加 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code>。</li>

<li style="margin:5px 0;line-height:1.75;">对完整字符串按 UTF-8 编码计算 MD5。</li>

<li style="margin:5px 0;line-height:1.75;">将 MD5 十六进制结果转换为大写,得到 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">sign</code>。</li>

</ol>

<p style="margin:10px 0;line-height:1.8;">示意:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-text" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">{APP_SECRET}

+ apirux.open.auth.token.create

+ app_key{APP_KEY}

+ code{CODE}

+ timestamp{TIMESTAMP}

+ {APP_SECRET}

</code></pre>

<p style="margin:10px 0;line-height:1.8;">签名时使用参数的<strong>原始值</strong>。得到 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">sign</code> 后,再对 HTTP 请求中的字段值进行 URL 编码。</p>

<h2 id="section-6" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">7. 第四步:调用换令牌接口</h2>

<p style="margin:10px 0;line-height:1.8;">请求信息:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-http" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">POST /api/route HTTP/1.1

Host: gw-open.zhanfubrowser.com

Content-Type: application/x-www-form-urlencoded; charset=utf-8

Accept: application/json


api=rux.open.auth.token.create&app_key=your_app_key&code=your_code&timestamp=1603293524&sign=YOUR_SIGN

</code></pre>

<h3 id="php" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">7.1 PHP 示例</h3>

<p style="margin:10px 0;line-height:1.8;">运行要求:PHP 7.4+,启用 cURL 扩展。</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-php" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;"><?php


const OPEN_API_URL = 'https://gw-open.zhanfubrowser.com/api/route';


function createSign(array $params, string $appSecret): string

{

   foreach ($params as $key => $value) {

       if ($key === '' || $value === null || $value === '') {

           unset($params[$key]);

           continue;

       }

       if (is_array($value)) {

           $params[$key] = json_encode(

               $value,

               JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES

           );

       } else {

           $params[$key] = (string) $value;

       }

   }


   ksort($params, SORT_STRING);

   $source = $appSecret;

   foreach ($params as $key => $value) {

       $source .= $key . $value;

   }

   $source .= $appSecret;


   return strtoupper(md5($source));

}


function exchangeCode(string $code): array

{

   $appKey = getenv('ZHANFU_APP_KEY');       // 开放平台分配

   $appSecret = getenv('ZHANFU_APP_SECRET'); // 仅保存在服务端


   if (!$appKey || !$appSecret || $code === '') {

       throw new RuntimeException('缺少 app_key、app_secret 或 code');

   }


   $params = [

       'api' => 'rux.open.auth.token.create', // 接口固定值

       'app_key' => $appKey,                  // 开放平台分配

       'code' => $code,                       // 授权回调参数

       'timestamp' => (string) time(),         // 服务端当前 Unix 秒

   ];

   $params['sign'] = createSign($params, $appSecret);


   $curl = curl_init(OPEN_API_URL);

   curl_setopt_array($curl, [

       CURLOPT_POST => true,

       CURLOPT_POSTFIELDS => http_build_query($params, '', '&', PHP_QUERY_RFC3986),

       CURLOPT_HTTPHEADER => [

           'Content-Type: application/x-www-form-urlencoded; charset=utf-8',

           'Accept: application/json',

       ],

       CURLOPT_RETURNTRANSFER => true,

       CURLOPT_TIMEOUT => 15,

   ]);


   $body = curl_exec($curl);

   if ($body === false) {

       throw new RuntimeException(curl_error($curl));

   }

   $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);

   curl_close($curl);


   if ($status < 200 || $status >= 300) {

       throw new RuntimeException("HTTP {$status}: {$body}");

   }


   return json_decode($body, true, 512, JSON_THROW_ON_ERROR);

}


$code = $_GET['code'] ?? ''; // 来自 OAuth 回调地址

$result = exchangeCode($code);

// 请加密保存 $result['data'] 中的令牌,不要原样写日志。

</code></pre>

<h3 id="java" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">7.2 Java 示例</h3>

<p style="margin:10px 0;line-height:1.8;">运行要求:Java 11+。</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-java" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">import java.net.URI;

import java.net.URLEncoder;

import java.net.http.HttpClient;

import java.net.http.HttpRequest;

import java.net.http.HttpResponse;

import java.nio.charset.StandardCharsets;

import java.security.MessageDigest;

import java.time.Instant;

import java.util.Map;

import java.util.TreeMap;

import java.util.stream.Collectors;


public final class ZhanfuOAuthExample {

   private static final String OPEN_API_URL =

       "https://gw-open.zhanfubrowser.com/api/route";


   static String createSign(Map<String, String> input, String appSecret)

       throws Exception {

       Map<String, String> sorted = new TreeMap<>();

       input.forEach((key, value) -> {

           if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {

               sorted.put(key, value);

           }

       });


       StringBuilder source = new StringBuilder(appSecret);

       sorted.forEach((key, value) -> source.append(key).append(value));

       source.append(appSecret);


       byte[] digest = MessageDigest.getInstance("MD5")

           .digest(source.toString().getBytes(StandardCharsets.UTF_8));

       StringBuilder hex = new StringBuilder();

       for (byte b : digest) {

           hex.append(String.format("%02X", b & 0xff));

       }

       return hex.toString();

   }


   static String formEncode(Map<String, String> params) {

       return params.entrySet().stream()

           .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)

               + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))

           .collect(Collectors.joining("&"));

   }


   static String exchangeCode(String code) throws Exception {

       String appKey = System.getenv("ZHANFU_APP_KEY");       // 开放平台分配

       String appSecret = System.getenv("ZHANFU_APP_SECRET"); // 仅保存在服务端

       if (appKey == null || appSecret == null || code == null || code.isEmpty()) {

           throw new IllegalArgumentException("缺少 app_key、app_secret 或 code");

       }


       Map<String, String> params = new TreeMap<>();

       params.put("api", "rux.open.auth.token.create"); // 接口固定值

       params.put("app_key", appKey);                   // 开放平台分配

       params.put("code", code);                        // 授权回调参数

       params.put("timestamp", Long.toString(Instant.now().getEpochSecond()));

       params.put("sign", createSign(params, appSecret));


       HttpRequest request = HttpRequest.newBuilder(URI.create(OPEN_API_URL))

           .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")

           .header("Accept", "application/json")

           .POST(HttpRequest.BodyPublishers.ofString(formEncode(params)))

           .build();


       HttpResponse<String> response = HttpClient.newHttpClient().send(

           request,

           HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)

       );

       if (response.statusCode() < 200 || response.statusCode() >= 300) {

           throw new IllegalStateException(

               "HTTP " + response.statusCode() + ": " + response.body()

           );

       }

       return response.body(); // 使用项目 JSON 库解析并安全保存 data 中的令牌

   }

}

</code></pre>

<h3 id="python" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">7.3 Python 示例</h3>

<p style="margin:10px 0;line-height:1.8;">运行要求:Python 3.9+,仅使用标准库。</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-python" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">import hashlib

import json

import os

import time

import urllib.parse

import urllib.request


OPEN_API_URL = "https://gw-open.zhanfubrowser.com/api/route"



def normalize(value):

   if isinstance(value, (list, dict)):

       return json.dumps(value, ensure_ascii=False, separators=(",", ":"))

   return str(value)



def create_sign(params: dict, app_secret: str) -> str:

   normalized = {

       str(key): normalize(value)

       for key, value in params.items()

       if key != "" and value is not None and value != ""

   }

   source = app_secret + "".join(

       key + normalized[key] for key in sorted(normalized)

   ) + app_secret

   return hashlib.md5(source.encode("utf-8")).hexdigest().upper()



def exchange_code(code: str) -> dict:

   app_key = os.environ["ZHANFU_APP_KEY"]       # 开放平台分配

   app_secret = os.environ["ZHANFU_APP_SECRET"] # 仅保存在服务端

   if not code:

       raise ValueError("缺少授权回调参数 code")


   params = {

       "api": "rux.open.auth.token.create", # 接口固定值

       "app_key": app_key,                   # 开放平台分配

       "code": code,                         # 授权回调参数

       "timestamp": str(int(time.time())),   # 服务端当前 Unix 秒

   }

   params["sign"] = create_sign(params, app_secret)


   body = urllib.parse.urlencode(params).encode("utf-8")

   request = urllib.request.Request(

       OPEN_API_URL,

       data=body,

       method="POST",

       headers={

           "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",

           "Accept": "application/json",

       },

   )

   with urllib.request.urlopen(request, timeout=15) as response:

       return json.loads(response.read().decode("utf-8"))



# code 应从应用自己的 OAuth 回调请求中读取。

result = exchange_code("CODE_FROM_CALLBACK")

# 请加密保存 result["data"] 中的令牌,不要原样写日志。

</code></pre>

<h3 id="go" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">7.4 Go 示例</h3>

<p style="margin:10px 0;line-height:1.8;">运行要求:Go 1.20+,仅使用标准库。</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-go" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">package main


import (

"crypto/md5"

"encoding/hex"

"encoding/json"

"fmt"

"io"

"net/http"

"net/url"

"os"

"sort"

"strings"

"time"

)


const openAPIURL = "https://gw-open.zhanfubrowser.com/api/route"


func createSign(params map[string]string, appSecret string) string {

keys := make([]string, 0, len(params))

for key, value := range params {

if key != "" && value != "" {

keys = append(keys, key)

}

}

sort.Strings(keys)


var source strings.Builder

source.WriteString(appSecret)

for _, key := range keys {

source.WriteString(key)

source.WriteString(params[key])

}

source.WriteString(appSecret)


digest := md5.Sum([]byte(source.String()))

return strings.ToUpper(hex.EncodeToString(digest[:]))

}


func exchangeCode(code string) (map[string]any, error) {

appKey := os.Getenv("ZHANFU_APP_KEY")       // 开放平台分配

appSecret := os.Getenv("ZHANFU_APP_SECRET") // 仅保存在服务端

if appKey == "" || appSecret == "" || code == "" {

return nil, fmt.Errorf("缺少 app_key、app_secret 或 code")

}


params := map[string]string{

"api":       "rux.open.auth.token.create", // 接口固定值

"app_key":   appKey,                       // 开放平台分配

"code":      code,                         // 授权回调参数

"timestamp": fmt.Sprint(time.Now().Unix()), // 服务端当前 Unix 秒

}

params["sign"] = createSign(params, appSecret)


form := url.Values{}

for key, value := range params {

form.Set(key, value)

}

req, err := http.NewRequest(http.MethodPost, openAPIURL, strings.NewReader(form.Encode()))

if err != nil {

return nil, err

}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")

req.Header.Set("Accept", "application/json")


client := &http.Client{Timeout: 15 * time.Second}

resp, err := client.Do(req)

if err != nil {

return nil, err

}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)

if err != nil {

return nil, err

}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {

return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)

}


var result map[string]any

if err := json.Unmarshal(body, &result); err != nil {

return nil, err

}

return result, nil

}


func main() {

result, err := exchangeCode("CODE_FROM_CALLBACK")

if err != nil {

panic(err)

}

_ = result // 加密保存 data 中的令牌,不要原样写日志。

}

</code></pre>

<h2 id="section-7" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">8. 成功响应</h2>

<p style="margin:10px 0;line-height:1.8;">示例:</p>

<pre style="box-sizing:border-box;margin:14px 0;padding:14px 16px;border:1px solid #d8dee4;border-radius:4px;background:#f6f8fa;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;"><code class="language-json" style="padding:0;background:transparent;color:#24292f;font-family:Consolas,Monaco,monospace;font-size:13px;">{

 "errorCode": 0,

 "message": "登录成功",

 "data": {

   "access_token": "ACCESS_TOKEN_VALUE",

   "refresh_token": "REFRESH_TOKEN_VALUE",

   "access_expires": 10080,

   "refresh_expires": 20160

 }

}

</code></pre>

<p style="margin:10px 0;line-height:1.8;">字段说明:</p>

<table style="width:100%;margin:14px 0;border-collapse:collapse;border-spacing:0;font-size:14px;line-height:1.6;">

<thead>

<tr>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">字段</th>

<th style="padding:9px 12px;border:1px solid #cfd7df;background:#f3f6f9;color:#1f2328;text-align:left;font-weight:700;vertical-align:top;">说明</th>

</tr>

</thead>

<tbody>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">errorCode</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">0</code> 表示本次业务调用成功;其他值按平台错误码处理</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">message</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">接口处理结果说明</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">data.access_token</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">访问受保护 API 使用的令牌</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">data.refresh_token</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">刷新令牌,使用方式以对应刷新接口文档为准</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">data.access_expires</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">访问令牌有效期数值;原始资料未明确单位,应以平台实际接口定义为准</td>

</tr>

<tr>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">data.refresh_expires</code></td>

<td style="padding:9px 12px;border:1px solid #cfd7df;vertical-align:top;">刷新令牌有效期数值;原始资料未明确单位,应以平台实际接口定义为准</td>

</tr>

</tbody>

</table>

<p style="margin:10px 0;line-height:1.8;">原始资料同时给出了“授权成功后 15 天内有效”的说明。授权关系有效期与令牌响应中的有效期字段可能不是同一概念,接入方应以接口实际返回值和平台最新规则为准,不要在代码中固定写死天数。</p>

<h2 id="section-8" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">9. 令牌保存与过期处理</h2>

<ul style="margin:10px 0;padding-left:26px;">

<li style="margin:5px 0;line-height:1.75;">在数据库或密钥管理服务中加密保存 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_token</code> 和 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">refresh_token</code>。</li>

<li style="margin:5px 0;line-height:1.75;">令牌应与站斧授权主体、应用和本地业务账号建立明确关联。</li>

<li style="margin:5px 0;line-height:1.75;">日志只记录令牌摘要或末尾少量字符,不记录完整令牌。</li>

<li style="margin:5px 0;line-height:1.75;">在令牌到期前按平台提供的刷新接口处理;若没有可用刷新接口,重新引导用户授权。</li>

<li style="margin:5px 0;line-height:1.75;">业务响应即使 HTTP 状态为 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">200</code>,仍应检查 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">errorCode</code>。</li>

<li style="margin:5px 0;line-height:1.75;">不要把 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_expires</code> 或 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">refresh_expires</code> 的单位写死,除非已从正式接口文档确认。</li>

</ul>

<h2 id="section-9" style="margin:32px 0 14px;padding:0 0 9px;border-bottom:1px solid #d8dee4;color:#1f2328;font-size:22px;line-height:1.4;font-weight:700;">10. 常见问题</h2>

<h3 id="section-10" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">签名不正确</h3>

<p style="margin:10px 0;line-height:1.8;">依次检查:</p>

<ol style="margin:10px 0;padding-left:26px;">

<li style="margin:5px 0;line-height:1.75;">是否误把 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">sign</code> 或 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_secret</code> 本身加入了待签名参数。</li>

<li style="margin:5px 0;line-height:1.75;">参数是否按名称升序排列。</li>

<li style="margin:5px 0;line-height:1.75;">是否漏签了业务参数。</li>

<li style="margin:5px 0;line-height:1.75;">是否在签名前进行了 URL 编码。</li>

<li style="margin:5px 0;line-height:1.75;">数组是否转换为无多余空格的 JSON 字符串后再签名。</li>

<li style="margin:5px 0;line-height:1.75;">是否按 UTF-8 计算 MD5,并将结果转为大写。</li>

<li style="margin:5px 0;line-height:1.75;">签名使用的值是否与实际发送的值完全一致。</li>

</ol>

<h3 id="code" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code> 无效或过期</h3>

<p style="margin:10px 0;line-height:1.8;"><code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">code</code> 有效期为 10 分钟。应检查服务器时间、回调处理耗时,并确保使用的是当前应用 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_key</code> 对应的授权码。</p>

<h3 id="section-11" style="margin:26px 0 12px;color:#1f2328;font-size:18px;line-height:1.45;font-weight:700;">已获得令牌但接口仍提示未授权</h3>

<p style="margin:10px 0;line-height:1.8;">检查请求中的 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">access_token</code> 是否属于当前 <code style="padding:2px 5px;border-radius:3px;background:#f3f4f6;color:#b42318;font-family:Consolas,Monaco,monospace;font-size:0.92em;">app_key</code>,授权关系是否仍有效,以及接口是否要求额外业务权限。</p>


</div>