<!-- 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×tamp=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>



