OpenAPI 第三方开发者手册
OpenAPI 第三方开发者手册
适用人群:接入 GodoCloud 平台的第三方系统开发者
1. 架构总览
① AppKey + 签名 → POST /openapi/token → access_token(应用凭证)
②A access_token + 用户名密码 → POST /openapi/user/login → user_token(用户JWT)
②B access_token + AppSecret二次签名 → POST /openapi/user/token → 免登录签发 user_token
③ user_token → Authorization: Bearer → 调用 /api/v1/<业务原路径>💡
user_token不能直接打到站内/user/*路由,必须通过 OpenAPI 业务网关前缀/api/v1/透传。 例如要列文件,业务原路径是/user/files/read,第三方实际调用:GET /api/v1/user/files/read?path=/网关会按顺序完成:user_token 校验 → 应用状态 / IP 白名单 / 限流 / scope 校验 → 重写路径 → 调用业务 handler,第三方不需要关心站内中间件细节。
2. 快速开始(5 步对接)
① 向 GodoCloud 管理员申请 AppKey / AppSecret
② POST /openapi/token → 拿到 access_token
③A POST /openapi/user/login → 用用户名密码换取 user_token
③B POST /openapi/user/token → 后台开启“免密Token”后,服务端免登录签发 user_token
④ Authorization: Bearer user_token → 调用 /api/v1/<业务原路径>
⑤ POST /openapi/user/token/refresh → token 即将过期前续期
③A和③B二选一即可。/openapi/user/token适合第三方可信服务端已完成自身登录、只需要映射到 GodoCloud 用户身份的场景。
3. 签名算法(HMAC-SHA256)
3.1 算法步骤
- 收集业务参数,排除
sign、timestamp、nonce。 - 按 key 字典序排序,以
key=value用&拼接,不做 URL encode。 - 末尾追加
×tamp=<秒>&nonce=<随机串>。 sign = HEX( HMAC_SHA256(signString, AppSecret) )(小写 hex)。
不同接口参与签名的业务参数不同:
| 接口 | 参与签名的业务参数 |
|---|---|
/openapi/token | app_key |
/openapi/sign/verify | app_key |
/openapi/user/token | user_id 或 username 二选一 |
3.2 示例
获取 access_token:
待签名串: app_key=ak_abc123×tamp=1717150000&nonce=8f3a7c9e
sign = hmac_sha256(上述串, AppSecret) → 64位小写hex免登录签发 user_token:
待签名串: username=alice×tamp=1717150000&nonce=8f3a7c9e
sign = hmac_sha256(上述串, AppSecret) → 64位小写hex也可以使用用户 ID 签名:
待签名串: user_id=100×tamp=1717150000&nonce=8f3a7c9e
sign = hmac_sha256(上述串, AppSecret) → 64位小写hex3.3 各语言签名函数
JavaScript / Node.js
const crypto = require('crypto')
function buildSign(params, timestamp, nonce, secret) {
const keys = Object.keys(params).filter(k => k && k !== 'sign').sort()
let raw = keys.map(k => `${k}=${params[k]}`).join('&')
if (raw) raw += '&'
raw += `timestamp=${timestamp}&nonce=${nonce}`
return crypto.createHmac('sha256', secret).update(raw).digest('hex')
}Go
func buildSign(params map[string]string, ts, nonce, secret string) string {
keys := make([]string, 0)
for k := range params { if k != "" && k != "sign" { keys = append(keys, k) } }
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
if b.Len() > 0 { b.WriteByte('&') }
b.WriteString(k + "=" + params[k])
}
if b.Len() > 0 { b.WriteByte('&') }
b.WriteString("timestamp=" + ts + "&nonce=" + nonce)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(b.String()))
return hex.EncodeToString(h.Sum(nil))
}Python
import hmac, hashlib
def build_sign(params, ts, nonce, secret):
keys = sorted(k for k in params if k and k != 'sign')
raw = '&'.join(f'{k}={params[k]}' for k in keys)
if raw: raw += '&'
raw += f'timestamp={ts}&nonce={nonce}'
return hmac.new(secret.encode(), raw.encode(), hashlib.sha256).hexdigest()3.4 校验规则
| 字段 | 规则 |
|---|---|
timestamp | Unix 秒;与服务器偏差 ≤ 300 秒 |
nonce | 任意随机串;5 分钟内不可重复 |
sign | 64 位小写 hex |
3.5 调试签名
POST /openapi/sign/verify(参数与 /openapi/token 一致),返回服务端计算的签名串供比对:
{
"code": 0,
"data": {
"sign_str": "app_key=ak_abc×tamp=1717150000&nonce=xxx",
"expected_sign": "服务端计算的sign",
"received_sign": "你提交的sign",
"match": true
}
}4. OpenAPI 网关接口
接口总览
| Method | Path | 鉴权 | 说明 |
|---|---|---|---|
| GET | /openapi/ping | 无 | 连通性测试 |
| POST | /openapi/sign/verify | 签名 | 调试签名 |
| POST | /openapi/token | 签名 | 获取 access_token |
| POST | /openapi/user/login | access_token | 用户名密码换取 user_token |
| POST | /openapi/user/token | access_token + AppSecret签名 | 免登录签发 user_token |
| POST | /openapi/user/token/refresh | access_token + user_token | 续期 user_token |
| GET | /openapi/user/info | access_token + user_token | 获取用户信息 |
4.1 POST /openapi/token — 获取 access_token
请求(form-urlencoded 或 query):
| 参数 | 必填 | 说明 |
|---|---|---|
app_key | ✅ | 管理员分配 |
timestamp | ✅ | Unix 秒 |
nonce | ✅ | 随机串 |
sign | ✅ | HMAC-SHA256 签名 |
响应:
{
"code": 0, "msg": "success",
"data": {
"access_token": "3a2b9c8e...",
"expires_in": 7200,
"expires_at": 1717157200,
"scopes": "*",
"token_type": "Bearer"
}
}4.2 POST /openapi/user/login — 换取 user_token
Header:
| 字段 | 说明 |
|---|---|
X-App-Key | AppKey |
X-Access-Token | access_token |
Body (JSON):
{
"username": "alice",
"password": "sha256后的密码或明文",
"plain": true
}plain=true:password 传原始明文(仅 IP 白名单内推荐)plain=false(默认):password 传sha256(原始密码)的 hex
响应:
{
"code": 0, "data": {
"user_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"user": { "id": 100, "username": "alice", "nickname": "Alice", "email": "", "phone": "", "avatar": "", "dept_id": 3, "role_id": 2 }
}
}4.3 POST /openapi/user/token — 免登录签发 user_token
用于第三方可信服务端在不提交 GodoCloud 用户密码的情况下,为指定 GodoCloud 用户签发 user_token。
前置条件:
- 已通过
/openapi/token获取有效access_token - 后台 OpenAPI 应用已开启“免密Token”(即
allow_user_token=1) - 第三方服务端必须持有
AppSecret,并对本次请求做二次签名 - 请求用户必须存在且未禁用
Header:
| 字段 | 必填 | 说明 |
|---|---|---|
X-App-Key | ✅ | AppKey |
X-Access-Token | ✅ | access_token |
Body (JSON):
| 字段 | 必填 | 说明 |
|---|---|---|
user_id | 与 username 二选一 | GodoCloud 用户 ID |
username | 与 user_id 二选一 | 用户名 / 手机号 / 邮箱 |
timestamp | ✅ | Unix 秒 |
nonce | ✅ | 随机串,5 分钟内不可重复 |
sign | ✅ | HMAC-SHA256 签名 |
user_id和username必须二选一,不能同时传。签名时只把实际使用的用户标识字段放入业务参数。
签名参数:
使用 username 时:
username=alice×tamp=1717150000&nonce=8f3a7c9e使用 user_id 时:
user_id=100×tamp=1717150000&nonce=8f3a7c9e请求示例:
POST /openapi/user/token
X-App-Key: ak_xxxxxx
X-Access-Token: 3a2b9c8e...
Content-Type: application/json{
"username": "alice",
"timestamp": "1717150000",
"nonce": "8f3a7c9e",
"sign": "64位小写hex签名"
}或:
{
"user_id": 100,
"timestamp": "1717150000",
"nonce": "8f3a7c9e",
"sign": "64位小写hex签名"
}响应:
{
"code": 0,
"data": {
"user_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"user": {
"id": 100,
"username": "alice",
"nickname": "Alice",
"email": "",
"phone": "",
"avatar": "",
"dept_id": 3,
"role_id": 2
}
}
}常见失败情况:
| 错误码 | 场景 |
|---|---|
40001 | user_id / username 未传、同时传,或 timestamp 格式错误 |
40110 | 时间戳超过 5 分钟 |
40111 | nonce 重复 |
40112 | 签名错误 |
40113 | access_token 无效、过期,或不属于当前 AppKey |
40301 | 后台未开启“免密Token”,即 allow_user_token disabled |
40302 | 用户已禁用 |
40401 | 用户不存在 |
4.4 POST /openapi/user/token/refresh — 续期
Header:X-App-Key + X-Access-Token + Authorization: Bearer <旧 user_token>
{ "code": 0, "data": { "user_token": "新的JWT", "token_type": "Bearer" } }4.5 GET /openapi/user/info — 用户信息
Header:同 4.4。返回用户详细资料(含角色、部门、岗位等)。
4.6 GET /openapi/ping — 连通性
{ "code": 0, "data": { "server_time": 1717150000, "version": "1.0" } }5. 业务接口(user_token 可调用)
拿到 user_token 后,统一在业务原路径前加 /api/v1/ 即可。 GodoCloud 内部任何 NeedAuth=1 的 /user/*、/workflow/*、/store/* 等都能透传调用。
5.1 调用方式
业务原路径例如 /user/files/read?path=/,第三方应调用:
GET /api/v1/user/files/read?path=/
Authorization: Bearer <user_token>支持的 token 传递方式(任选其一):
| 方式 | 示例 |
|---|---|
| Authorization(推荐) | Authorization: Bearer <user_token> |
| Header(旧版) | token: <user_token> |
| URL Query | ?token=<user_token> |
⚠️ 网关禁止透传以下前缀(防越权 / 防递归):
/openapi/、/api/v1/、/admin/、/install/。 命中将返回40301 scope forbidden。
5.2 业务接口完整清单
📁 文件 / 网盘(/user/files/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/files/desktop | 获取桌面文件 |
| GET | /user/files/read | 读取目录 |
| GET | /user/files/stat | 获取文件信息 |
| POST | /user/files/chmod | 修改文件权限 |
| GET | /user/files/exists | 判断文件是否存在 |
| GET | /user/files/readfile | 读取文件内容(带密) |
| GET | /user/files/unlink | 删除文件 |
| GET | /user/files/pwd | 加密文件 |
| GET | /user/files/unpwd | 解密文件 |
| GET | /user/files/clear | 清空目录 |
| GET | /user/files/rename | 文件重命名 |
| POST | /user/files/mkdir | 创建目录 |
| GET | /user/files/rmdir | 删除目录 |
| GET | /user/files/restore | 恢复文件 |
| GET | /user/files/copyfile | 复制文件 |
| GET | /user/files/favorite | 收藏文件 |
| POST | /user/files/writefile | 写文件 |
| POST | /user/files/appendfile | 追加文件 |
| GET | /user/files/search | 搜索文件 |
| GET | /user/files/zip | 压缩文件 |
| GET | /user/files/unzip | 解压文件 |
| POST | /user/files/share | 共享文件 |
| GET | /user/files/sharelist | 共享文件列表 |
| GET | /user/files/knowledge | 将文件夹加入知识库 |
| POST | /user/files/ask | 询问知识库 |
| GET | /user/files/avatarlist | 获取用户头像列表 |
| GET | /user/files/saveavatar | 保存用户头像 |
🤝 协同编辑(/user/files/collaboration/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/files/collaboration/editusers | 获取编辑用户列表 |
| GET | /user/files/collaboration/timeline | 获取编辑时间线 |
| GET | /user/files/collaboration/sharers | 获取共享用户列表 |
| POST | /user/files/collaboration/recover | 从历史记录恢复文件 |
📤 文件上传(/user/upload*)
| Method | Path | 说明 |
|---|---|---|
| POST | /user/upload | 单文件上传 |
| POST | /user/multiupload | 多文件上传 |
| POST | /user/verifyupload | 校验文件上传 |
📝 表单(/user/form*、/user/editdata)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/forms | 获取表单列表 |
| GET | /user/formfields | 获取表单字段列表 |
| GET | /user/datalist | 获取表单数据列表 |
| POST | /user/editdata | 编辑用户数据 |
🔄 工作流(/workflow/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /workflow/list | 获取工作流列表 |
| GET | /workflow/addbefore | 工作流添加表单前 |
| POST | /workflow/adddata | 工作流添加数据 |
| GET | /workflow/getdata | 工作流获取数据 |
| GET | /workflow/datalist | 工作流数据列表 |
| GET | /workflow/myauditdatalist | 我审核的数据列表 |
| GET | /workflow/formdata | 获取前端表格 |
| GET | /workflow/getview | 获取流程图 |
| POST | /workflow/getusersbydeptids | 根据部门获取用户 |
| GET | /workflow/mychecklist | 需要审核的工作流列表 |
| GET | /workflow/getcheckdata | 审核人单条数据 |
| POST | /workflow/checkflowdata | 审核工作流节点 |
| GET | /workflow/getcopytomelist | 发送给我的抄送 |
| GET | /workflow/getcopyfrommelist | 我发送的抄送 |
| POST | /workflow/resubmitflowformdata | 重新编辑工作流审批 |
| GET | /workflow/getflowcheckcomments | 获取审核意见 |
| GET | /workflow/getFlowRelation | 获取流程类型和父子关系 |
| GET | /workflow/getParentFlowChildren | 获取父流程的子流程 |
| GET | /workflow/getWorkFlowListByType | 按类型获取流程列表 |
| GET | /workflow/exportdata | 导出申请数据 |
👤 用户 / 组织(/user/user*、/user/dept*、/user/role*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/list | 按部门或角色获取用户列表 |
| POST | /user/searchuser | 搜索用户 |
| GET | /user/sharelist | 分享用户列表 |
| GET | /user/depts | 按PID获取部门列表 |
| GET | /user/deptsall | 获取全部部门列表 |
| GET | /user/roles | 获取角色列表 |
👤 个人资料(/user/profile*、/user/savepwd)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/getprofile | 获取个人资料 |
| POST | /user/updateprofile | 更新个人资料 |
| POST | /user/savepwd | 修改密码 |
📖 词条 / 知识库(/user/entry*、/user/tag*、/user/category*)
| Method | Path | 说明 |
|---|---|---|
| POST | /user/entry/create | 创建词条 |
| GET | /user/entry/list | 获取词条列表 |
| GET | /user/entry/detail | 获取词条详情 |
| GET | /user/entry/category | 获取词条分类列表 |
| POST | /user/entry/view | 增加词条浏览量 |
| GET | /user/entry/top | 获取置顶词条 |
| GET | /user/entry/recommended | 获取推荐词条 |
| GET | /user/entry/hot | 获取热门词条 |
| GET | /user/tag/list | 获取标签列表 |
| GET | /user/category/list | 获取词条分类列表 |
| GET | /user/category/entry/list | 获取分类下词条列表 |
📊 图表(/user/charts/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/charts/list | 图表列表 |
| GET | /user/charts/getdata | 获取图表数据 |
📰 通知(/user/news/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/news/list | 查看通知列表 |
📖 字典(/user/dict/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/dict/cache/cate | 按分类编码获取字典缓存 |
| GET | /user/dict/cache/field | 按字段编码获取字典缓存 |
| GET | /user/dict/cate/tree | 获取字典分类树 |
| GET | /user/dict/cate/fields | 按分类ID获取字段列表 |
🌍 区域(/user/areas/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /user/areas/list | 获取区域列表(三级联动) |
| GET | /user/areas/tree | 获取区域树形结构 |
| GET | /user/areas/all | 获取全部区域(扁平映射) |
🏪 应用商店(/store/*)
| Method | Path | 说明 |
|---|---|---|
| GET | /store/storelist | 应用商店列表 |
🔗 钉钉空间
| Method | Path | 说明 |
|---|---|---|
| GET | /user/ding/space | 钉钉空间文件列表 |
6. 错误码
| 错误码 | 说明 | 处理建议 |
|---|---|---|
| 0 | 成功 | — |
| 40001 | 参数错误 | 检查必填参数 |
| 40101 | 应用不存在 | 检查 app_key |
| 40102 | 应用已禁用 | 联系管理员启用 |
| 40103 | IP 不在白名单 | 联系管理员加白 |
| 40110 | 时间戳过期 | 同步服务器时间 |
| 40111 | nonce 重复 | 更换随机串 |
| 40112 | 签名错误 | 参照第 3 节排查 |
| 40113 | token 无效 | 重新获取 |
| 40114 | token 过期 | 重新获取 |
| 40301 | scope 无权限 / 透传路径被禁 | 联系管理员授权;或检查是否调用了 /openapi/ /admin/ /install/ 等禁止透传的路径 |
| 40302 | 用户已禁用 | 联系管理员 |
| 40401 | 用户不存在 | 检查用户名 |
| 40402 | 密码错误 | 检查密码 |
| 42901 | 请求频率超限 | 降低频率或联系管理员提额 |
| 50001 | 服务器内部错误 | 联系管理员查看日志 |
7. 完整对接示例
7.1 Node.js 示例
const crypto = require('crypto')
const axios = require('axios')
const BASE = 'https://your-godocloud.com'
const APP_KEY = 'ak_xxxxxx'
const APP_SECRET = 'sk_xxxxxx'
// ① 签名函数
function buildSign(params, ts, nonce) {
const keys = Object.keys(params).filter(k => k && k !== 'sign').sort()
let raw = keys.map(k => `${k}=${params[k]}`).join('&')
if (raw) raw += '&'
raw += `timestamp=${ts}&nonce=${nonce}`
return crypto.createHmac('sha256', APP_SECRET).update(raw).digest('hex')
}
async function main() {
// ② 获取 access_token
const ts = Math.floor(Date.now() / 1000).toString()
const nonce = crypto.randomBytes(8).toString('hex')
const sign = buildSign({ app_key: APP_KEY }, ts, nonce)
const tokenRes = await axios.post(`${BASE}/openapi/token`, null, {
params: { app_key: APP_KEY, timestamp: ts, nonce, sign }
})
const accessToken = tokenRes.data.data.access_token
// ③ 免登录签发 user_token(后台需开启“免密Token”)
const userTs = Math.floor(Date.now() / 1000).toString()
const userNonce = crypto.randomBytes(8).toString('hex')
const username = 'alice'
const userSign = buildSign({ username }, userTs, userNonce)
const userTokenRes = await axios.post(`${BASE}/openapi/user/token`,
{ username, timestamp: userTs, nonce: userNonce, sign: userSign },
{ headers: { 'X-App-Key': APP_KEY, 'X-Access-Token': accessToken } }
)
const userToken = userTokenRes.data.data.user_token
// 如未开启“免密Token”,也可以改用 /openapi/user/login:
// const loginRes = await axios.post(`${BASE}/openapi/user/login`,
// { username: 'alice', password: '原始密码', plain: true },
// { headers: { 'X-App-Key': APP_KEY, 'X-Access-Token': accessToken } }
// )
// const userToken = loginRes.data.data.user_token
// ④ 调用业务接口(业务原路径前面统一加 /api/v1)
const files = await axios.get(`${BASE}/api/v1/user/files/read`, {
params: { path: '/' },
headers: { Authorization: `Bearer ${userToken}` }
})
console.log('文件列表:', files.data)
// ⑤ 调用工作流接口
const workflows = await axios.get(`${BASE}/api/v1/workflow/list`, {
headers: { Authorization: `Bearer ${userToken}` }
})
console.log('工作流列表:', workflows.data)
}
main().catch(console.error)7.2 Python 示例
import hmac, hashlib, time, random, requests
BASE = 'https://your-godocloud.com'
APP_KEY = 'ak_xxxxxx'
APP_SECRET = 'sk_xxxxxx'
def build_sign(params, ts, nonce):
keys = sorted(k for k in params if k and k != 'sign')
raw = '&'.join(f'{k}={params[k]}' for k in keys)
if raw: raw += '&'
raw += f'timestamp={ts}&nonce={nonce}'
return hmac.new(APP_SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest()
# ① 获取 access_token
ts = str(int(time.time()))
nonce = ''.join(random.choices('abcdef0123456789', k=16))
sign = build_sign({'app_key': APP_KEY}, ts, nonce)
token_res = requests.post(f'{BASE}/openapi/token', params={
'app_key': APP_KEY, 'timestamp': ts, 'nonce': nonce, 'sign': sign
})
access_token = token_res.json()['data']['access_token']
# ② 免登录签发 user_token(后台需开启“免密Token”)
user_ts = str(int(time.time()))
user_nonce = ''.join(random.choices('abcdef0123456789', k=16))
username = 'alice'
user_sign = build_sign({'username': username}, user_ts, user_nonce)
user_token_res = requests.post(f'{BASE}/openapi/user/token', json={
'username': username,
'timestamp': user_ts,
'nonce': user_nonce,
'sign': user_sign,
}, headers={'X-App-Key': APP_KEY, 'X-Access-Token': access_token})
user_token = user_token_res.json()['data']['user_token']
# 如未开启“免密Token”,也可以改用 /openapi/user/login:
# login_res = requests.post(f'{BASE}/openapi/user/login', json={
# 'username': 'alice', 'password': '原始密码', 'plain': True
# }, headers={'X-App-Key': APP_KEY, 'X-Access-Token': access_token})
# user_token = login_res.json()['data']['user_token']
# ③ 调用业务接口(业务原路径前面统一加 /api/v1)
headers = {'Authorization': f'Bearer {user_token}'}
files = requests.get(f'{BASE}/api/v1/user/files/read', params={'path': '/'}, headers=headers)
print('文件列表:', files.json())
# 调用工作流
wf = requests.get(f'{BASE}/api/v1/workflow/list', headers=headers)
print('工作流:', wf.json())7.3 Go 示例
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
base = "https://your-godocloud.com"
appKey = "ak_xxxxxx"
appSecret = "sk_xxxxxx"
)
func buildSign(params map[string]string, ts, nonce string) string {
keys := make([]string, 0)
for k := range params { if k != "" && k != "sign" { keys = append(keys, k) } }
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
if b.Len() > 0 { b.WriteByte('&') }
b.WriteString(k + "=" + params[k])
}
if b.Len() > 0 { b.WriteByte('&') }
b.WriteString("timestamp=" + ts + "&nonce=" + nonce)
h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(b.String()))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
// ① 获取 access_token
ts := fmt.Sprintf("%d", time.Now().Unix())
nonceBuf := make([]byte, 8)
rand.Read(nonceBuf)
nonce := hex.EncodeToString(nonceBuf)
sign := buildSign(map[string]string{"app_key": appKey}, ts, nonce)
resp, _ := http.PostForm(base+"/openapi/token", url.Values{
"app_key": {appKey}, "timestamp": {ts}, "nonce": {nonce}, "sign": {sign},
})
var tokenResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&tokenResp)
accessToken := tokenResp["data"].(map[string]interface{})["access_token"].(string)
// ② 免登录签发 user_token(省略 JSON body 构造,参考 net/http 标准库)
// 调用 POST /openapi/user/token:
// - Header: X-App-Key + X-Access-Token
// - Body: username/user_id + timestamp + nonce + sign
// - sign 使用 AppSecret 对 username 或 user_id 做 HMAC-SHA256
fmt.Println("access_token:", accessToken)
// 接下来参照 Python/Node.js 示例的 /openapi/user/token 流程继续即可
}8. 注意事项
- 时间同步:确保你的服务器时间与 GodoCloud 服务器偏差 < 5 分钟。
- nonce 唯一性:建议使用 UUID 或随机 hex(≥ 16 字节),同一个 AppKey 下 5 分钟内不要重复。
- 免登录签发安全边界:
/openapi/user/token只能由第三方可信服务端调用,AppSecret绝不能下发到浏览器、移动端或小程序端。 - 后台开关:免登录签发必须由 GodoCloud 管理员在 OpenAPI 应用中开启“免密Token”(
allow_user_token=1);未开启会返回40301 allow_user_token disabled。 - 用户映射:第三方系统必须能可靠映射到 GodoCloud 用户,可使用
user_id,也可使用username(支持用户名 / 手机号 / 邮箱)。 - 密码登录兜底:如果不启用免登录签发,可继续使用
/openapi/user/login,生产环境推荐plain=false,客户端先做sha256(密码)再传输。 - HTTPS:所有请求必须走 HTTPS,避免凭证、token 或签名参数泄露。
- token 存储:
access_token和user_token都应安全存储,不要暴露在前端 URL、浏览器 LocalStorage 或日志中。 - 续期策略:
user_token快过期前调用/openapi/user/token/refresh,无需用户重新输入密码;免登录场景也可以重新调用/openapi/user/token签发。 - 错误重试:遇到
42901(限流)应退避重试;40113/40114需重新获取 token。 - 业务接口参数:具体参数请通过 GodoCloud 管理后台的“接口文档”或联系管理员获取。
9. 常见问题
Q1:为什么需要两次 token?
A:access_token 证明第三方应用身份;user_token 证明GodoCloud 用户身份。两者职责不同,安全隔离。
Q2:可以不用用户登录,直接拿到 user_token 吗?
A:可以。使用 POST /openapi/user/token,由第三方可信服务端携带 access_token,并使用 AppSecret 对 user_id 或 username 做二次签名,即可为指定 GodoCloud 用户签发 user_token。前提是管理员已在 OpenAPI 应用中开启“免密Token”(allow_user_token=1)。
Q3:免登录签发 user_token 是否需要用户密码?
A:不需要。第三方系统只需要能可靠映射到 GodoCloud 用户,例如使用 user_id、用户名、手机号或邮箱。该接口不接收也不校验用户密码。
Q4:为什么免登录签发接口还要签名?
A:因为该接口相当于“代用户登录”,权限较高。除 access_token 外,还需要使用 AppSecret 对本次请求做 HMAC-SHA256 签名,防止伪造请求、重放请求和越权签发。
Q5:免登录签发接口能在前端直接调用吗?
A:不能。/openapi/user/token 必须在可信服务端调用,因为签名需要使用 AppSecret。如果把 AppSecret 放到浏览器、移动端或小程序端,就等同于泄露应用密钥。
Q6:user_token 能调用管理员接口吗?
A:不能。user_token 只允许通过 /api/v1/ 网关访问业务接口;/admin/*、/install/*、/openapi/* 等前缀会被网关拒绝。
Q7:为什么不能直接 Authorization: Bearer <user_token> 打到 /user/files/read?
A:那是站内 Web 路由,会被站内 JWT 中间件拦截,且无法走 OpenAPI 应用层校验(IP 白名单、scope、限流等)。所有外部业务调用必须走 /api/v1/ 网关,例如:GET /api/v1/user/files/read?path=/。
Q8:用户修改密码后 user_token 还能用吗?
A:在 token 自然过期前通常仍可使用;过期后需要重新获取。免登录场景可重新调用 /openapi/user/token 签发,密码登录场景则需要重新调用 /openapi/user/login。
Q9:如何批量操作多个用户?
A:每个 GodoCloud 用户都需要独立的 user_token。免登录场景下,第三方可信服务端可以分别用不同的 user_id 或 username 调用 /openapi/user/token,然后使用对应 user_token 调用业务接口。
Q10:签名调试不通过怎么办?
A:先调用 /openapi/sign/verify 排查 /openapi/token 的签名;免登录签发 /openapi/user/token 时,要确认参与签名的业务参数只包含实际传入的 user_id 或 username,并且 timestamp、nonce 与请求体完全一致。
Q11:返回 40301 allow_user_token disabled 怎么办?
A:说明当前 OpenAPI 应用未开启“免密Token”。请在 GodoCloud 管理后台编辑对应 OpenAPI 应用,开启该开关后再调用 /openapi/user/token。
Q12:username 可以传什么?
A:可以传 GodoCloud 用户名,也可以传手机号或邮箱;如果你的系统已经保存了 GodoCloud 用户 ID,推荐优先使用 user_id,避免用户名、手机号、邮箱变更带来的映射问题。