适用平台:有声合成平台 / 掌维 AI 短剧 后端:Laravel,接口统一前缀
/api本文档覆盖前端需要改造对接的两块内容:接口签名与登录图片验证码,以及通用约定与错误码。
/api 开头,例如 POST /api/anime/detail。 d-token: <登录接口返回的 token>
d_token 读取令牌,新代码请统一使用请求头。d-token):GET /api/login、GET /api/captcha、ANY /api/video/seedanceCallback。成功:
{ "msg": "", "code": 0, "data": { } }
失败(业务异常):
{ "code": 1002, "msg": "签名异常", "data": { } }
约定:
code === 0表示成功,其余为业务错误码,前端按code判断并提示msg。 业务异常统一返回 HTTP 200;个别接口由框架做参数校验时可能返回 HTTP 422,建议在响应拦截器里一并处理。
| code | msg | 说明 |
|---|---|---|
| 0 | - | 成功 |
| 1002 | 签名异常 | 签名不正确 / 过期 / 缺少签名头 / 请求重复 |
| 1003 | 请确认填写的数据无误 | 参数错误 |
| 1005 | 没有权限 | 无权限访问该资源(含跨公司访问) |
| 1008 | 请先登录 | 未登录或 token 失效 |
| 10021 | 验证码错误 | 图片验证码错误或已过期 |
服务端在以下接口组启用了验签:
/api/deepseek/*(scriptList、scriptInfo、saveEpisodeContent 等)/api/AIGeneration/*/api/anime/*/api/canvas/*/api/book/uploadAudioEffect、/api/book/uploadBgm推荐做法:前端统一给所有请求加签名头,不必区分接口。 服务端对未开启验签的接口不会校验,多传请求头没有任何副作用,这样可以避免后续接口调整归属时前端漏改。
注意:服务端有开关,未开启验签时签名头会被忽略,接口照常返回。因此前端上线后如果发现一切正常,不代表签名已经生效,需要和后端确认开关状态。
三个请求头:
| 请求头 | 内容 |
|---|---|
X-Time |
当前时间戳(秒,10 位) |
X-Nonce |
前端生成的随机串,同一账号在时间窗内不可重复 |
X-Sign |
签名结果,小写十六进制(64 位) |
签名串按固定顺序拼接,字段间用 | 分隔:
{请求方法大写}|{接口路径}|{X-Time}|{X-Nonce}|d-token={d-token}
签名算法:
X-Sign = HMAC-SHA256(签名串, SIGN_SALT) // 结果转小写 hex
其中 SIGN_SALT 由后端提供,需在打包时注入前端配置。
接口路径(PATH)的规则,这是最容易算错的地方:
/ 开头?a=1&b=2 要丢掉)/api/anime/detail示例:
请求:POST /api/anime/detail?anime_id=123
X-Time = 1737280000
X-Nonce = abc123
d-token = 0123456789abcdef0123456789abcdef
签名串 = POST|/api/anime/detail|1737280000|abc123|d-token=0123456789abcdef0123456789abcdef
X-Sign = HMAC-SHA256(签名串, SIGN_SALT)
有效期与重放保护:
X-Time 与本机时间偏差超过 120 秒(可由后端配置)判定为过期;X-Nonce 在时间窗内只能使用一次,重复请求会返回 1002。用下面的固定输入自测,输出的 X-Sign 必须与表中一致,否则说明拼接规则或编码有误。
注意:用例中的时间戳是固定值(已过期),仅用于校验算法实现,不能用于真实请求。真实请求必须使用当前时间。
| 项 | 用例一 | 用例二 |
|---|---|---|
| 请求方法 | POST |
GET |
| 接口路径 | /api/anime/detail |
/api/canvas/list |
| X-Time | 1737280000 |
1737280000 |
| X-Nonce | abc123 |
nonce9876 |
| d-token | 0123456789abcdef0123456789abcdef |
fedcba9876543210fedcba9876543210 |
| SIGN_SALT | example_salt |
example_salt |
| X-Sign | f9a6c3cc7cf1b7f4b390fc08d91c5070445d60aadeba4b823509ea3092d7ae4b |
3bc9b788a618dc44f06dba1aa2b0e5ebb703166b7f2b496eef49233a6461c4eb |
可以直接用 Node 跑一遍验证:
// node sign-selfcheck.js
const crypto = require('crypto')
function sign(method, path, time, nonce, token, salt) {
const str = `${method}|${path}|${time}|${nonce}|d-token=${token}`
return crypto.createHmac('sha256', salt).update(str).digest('hex')
}
console.log(sign('POST', '/api/anime/detail', '1737280000', 'abc123',
'0123456789abcdef0123456789abcdef', 'example_salt'))
// 期望:f9a6c3cc7cf1b7f4b390fc08d91c5070445d60aadeba4b823509ea3092d7ae4b
console.log(sign('GET', '/api/canvas/list', '1737280000', 'nonce9876',
'fedcba9876543210fedcba9876543210', 'example_salt'))
// 期望:3bc9b788a618dc44f06dba1aa2b0e5ebb703166b7f2b496eef49233a6461c4eb
两条都输出一致,说明签名算法实现正确;剩下只需要保证 path、token 与真实请求对齐。
npm install crypto-js
// src/utils/sign.js
import HmacSHA256 from 'crypto-js/hmac-sha256'
import Hex from 'crypto-js/enc-hex'
const SIGN_SALT = process.env.VUE_APP_SIGN_SALT
/**
* 生成接口签名头
* @param {string} method 请求方法
* @param {string} path 接口路径(以 / 开头,不含域名与 query)
* @param {string} token 登录令牌
*/
export function buildSignHeaders(method, path, token) {
const time = Math.floor(Date.now() / 1000).toString()
const nonce = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
const str = `${method.toUpperCase()}|${path}|${time}|${nonce}|d-token=${token}`
return {
'X-Time': time,
'X-Nonce': nonce,
'X-Sign': HmacSHA256(str, SIGN_SALT).toString(Hex)
}
}
// src/utils/sign.js
const SIGN_SALT = process.env.VUE_APP_SIGN_SALT
const enc = new TextEncoder()
let cachedKey = null
async function getKey() {
if (!cachedKey) {
cachedKey = crypto.subtle.importKey(
'raw', enc.encode(SIGN_SALT), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
)
}
return cachedKey
}
export async function buildSignHeaders(method, path, token) {
const time = Math.floor(Date.now() / 1000).toString()
const nonce = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
const str = `${method.toUpperCase()}|${path}|${time}|${nonce}|d-token=${token}`
const sig = await crypto.subtle.sign('HMAC', await getKey(), enc.encode(str))
const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('')
return { 'X-Time': time, 'X-Nonce': nonce, 'X-Sign': hex }
}
Web Crypto 的
crypto.subtle只在 HTTPS 或 localhost 下可用,若前端部署在 http 环境请使用方案 A。
import { buildSignHeaders } from '@/utils/sign'
import store from '@/store'
service.interceptors.request.use(async (config) => {
if (config.skipSign) return config // 个别请求可跳过
const token = store.getters.token || ''
// 关键:path 必须是含 /api 前缀的真实路径,且不含 query
const rawUrl = String(config.url || '')
const path = '/' + rawUrl.split('?')[0].replace(/^\/+/, '')
const method = (config.method || 'get').toUpperCase()
const headers = await buildSignHeaders(method, path, token)
Object.assign(config.headers, headers)
return config
}, (error) => Promise.reject(error))
| 现象 | 排查方向 |
|---|---|
| 一直 1002 | 先跑第 2.3 节自检用例;再打印前端实际拼出的path,与后端 $request->path() 对比 |
| 偶发 1002 | 服务器与客户端时钟偏差超过 120 秒,检查服务器 NTP |
| 1002 且提示 nonce 重复 | 同一个X-Nonce 被复用(例如请求重试时复用了旧头),重试需重新生成 |
path 对不上 |
注意axios 的 baseURL:若 /api 写在 baseURL 里、url 只写了 /anime/detail,需手动补成 /api/anime/detail |
| token 对不上 | 签名里的d-token 必须与请求头 d-token 完全一致(含大小写),若 token 做过 trim 或转换需保持一致 |
GET /api/captcha
免登录,无需签名。返回:
{
"msg": "",
"code": 0,
"data": {
"captcha_key": "8f14e45fceea167a5a36dedd4bea2543",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
}
image:完整的 data URI,可直接作为 <img> 的 src,无需额外处理。服务端对该接口有 20 次/分钟/IP 的限流。
GET /api/login?account=<账号>&passwd=<密码>&captcha_key=<key>&captcha_code=<用户输入>
| 参数 | 必填 | 说明 |
|---|---|---|
account |
是 | 账号 |
passwd |
是 | 密码 |
captcha_key |
后端开启验证码时必填 | 获取验证码接口返回的key |
captcha_code |
后端开启验证码时必填 | 用户输入的验证码,大小写不敏感 |
成功返回:
{
"msg": "",
"code": 0,
"data": {
"uid": 12,
"nickname": "张三",
"token": "f3a8c1e0b7d94a2f8e6c5b1d0a9f3e72",
"role": "admin"
}
}
role 取值:superadmin(平台)、admin(公司管理员)、user(组员)。
key 立即失效,所以用户输错后必须自动刷新一张新验证码;code = 10021。服务端有总开关。开关未开启时,不传
captcha_key/captcha_code也能正常登录,因此前端可以先上线,等后端开启后再生效。
登录页:
GET /api/captcha,把 data.image 绑到 <img :src="captcha.image">;captcha_key 与 captcha_code;code === 10021 时,自动重新拉取验证码并提示用户重新输入;key 反复提交。GET /api/userInfo
需登录。返回 uid、nickname、token、role、points 等字段。
GET /api/logout
本文档涉及的两项能力都由后端开关控制,前端可以先行上线,行为与现在完全一致:
| 能力 | 后端开关 | 未开启时的表现 |
|---|---|---|
| 接口签名 | CHECK_SIGN |
签名头被忽略,按原逻辑放行 |
| 登录验证码 | CAPTCHA_ENABLED |
登录不校验验证码 |
建议的上线顺序
php artisan config:clear):
CHECK_SIGN,观察日志是否出现验签失败;CAPTCHA_ENABLED,确认登录流程正常。需要向后端确认的信息
SIGN_SALT 的正式值(需与前端打包配置一致,改动需两端同步);CHECK_SIGN 与 CAPTCHA_ENABLED 的开启时间点;| 日期 | 内容 |
|---|---|
| 2026-09-21 | 初版:接口签名(HMAC-SHA256 + nonce 防重放)、登录图片验证码 |