frontend-integration.md 14 KB

前端对接文档

适用平台:有声合成平台 / 掌维 AI 短剧 后端:Laravel,接口统一前缀 /api 本文档覆盖前端需要改造对接的两块内容:接口签名登录图片验证码,以及通用约定与错误码。


一、通用约定

1.1 接口地址与鉴权

  • 所有业务接口以 /api 开头,例如 POST /api/anime/detail
  • 登录后,除少量免登录接口外,均需在请求头携带登录令牌:
  d-token: <登录接口返回的 token>
  • 少数导出类接口服务端也兼容从参数 d_token 读取令牌,新代码请统一使用请求头
  • 免登录接口(不需要 d-token):GET /api/loginGET /api/captchaANY /api/video/seedanceCallback

1.2 响应格式

成功:

{ "msg": "", "code": 0, "data": { } }

失败(业务异常):

{ "code": 1002, "msg": "签名异常", "data": { } }

约定:code === 0 表示成功,其余为业务错误码,前端按 code 判断并提示 msg。 业务异常统一返回 HTTP 200;个别接口由框架做参数校验时可能返回 HTTP 422,建议在响应拦截器里一并处理。

1.3 常用错误码

code msg 说明
0 - 成功
1002 签名异常 签名不正确 / 过期 / 缺少签名头 / 请求重复
1003 请确认填写的数据无误 参数错误
1005 没有权限 无权限访问该资源(含跨公司访问)
1008 请先登录 未登录或 token 失效
10021 验证码错误 图片验证码错误或已过期

二、接口签名

2.1 适用范围

服务端在以下接口组启用了验签:

  • 剧本管理:/api/deepseek/*scriptListscriptInfosaveEpisodeContent 等)
  • AI 生成:/api/AIGeneration/*
  • 动漫管理:/api/anime/*
  • 画布模式:/api/canvas/*
  • 音频素材上传:/api/book/uploadAudioEffect/api/book/uploadBgm

推荐做法:前端统一给所有请求加签名头,不必区分接口。 服务端对未开启验签的接口不会校验,多传请求头没有任何副作用,这样可以避免后续接口调整归属时前端漏改。

注意:服务端有开关,未开启验签时签名头会被忽略,接口照常返回。因此前端上线后如果发现一切正常,不代表签名已经生效,需要和后端确认开关状态。

2.2 签名规则

三个请求头:

请求头 内容
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)的规则,这是最容易算错的地方:

  • / 开头
  • 不含协议和域名
  • 不含 query string?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。

2.3 自检用例(用于验证前端实现是否正确)

用下面的固定输入自测,输出的 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

两条都输出一致,说明签名算法实现正确;剩下只需要保证 pathtoken 与真实请求对齐。

2.4 前端实现

方案 A:使用 crypto-js(推荐,改动最小)

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)
  }
}

方案 B:使用浏览器原生 Web Crypto(不新增依赖)

// 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。

挂到 axios 拦截器

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))

2.5 签名相关常见错误

现象 排查方向
一直 1002 先跑第 2.3 节自检用例;再打印前端实际拼出的path,与后端 $request->path() 对比
偶发 1002 服务器与客户端时钟偏差超过 120 秒,检查服务器 NTP
1002 且提示 nonce 重复 同一个X-Nonce 被复用(例如请求重试时复用了旧头),重试需重新生成
path 对不上 注意axiosbaseURL:若 /api 写在 baseURL 里、url 只写了 /anime/detail,需手动补成 /api/anime/detail
token 对不上 签名里的d-token 必须与请求头 d-token 完全一致(含大小写),若 token 做过 trim 或转换需保持一致

三、登录图片验证码

3.1 获取验证码

GET /api/captcha

免登录,无需签名。返回:

{
  "msg": "",
  "code": 0,
  "data": {
    "captcha_key": "8f14e45fceea167a5a36dedd4bea2543",
    "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
  }
}
  • captcha_key:本次验证码的唯一标识,登录时回传;
  • image:完整的 data URI,可直接作为 <img>src,无需额外处理。

服务端对该接口有 20 次/分钟/IP 的限流。

3.2 登录

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(组员)。

3.3 验证码的关键行为

  • 有效期 5 分钟,过期后需重新获取;
  • 一次性消费:无论校验成功还是失败,key 立即失效,所以用户输错后必须自动刷新一张新验证码;
  • 大小写不敏感;
  • 错误返回 code = 10021

服务端有总开关。开关未开启时,不传 captcha_key / captcha_code 也能正常登录,因此前端可以先上线,等后端开启后再生效。

3.4 前端实现要点

登录页:

  1. 进入页面时调用 GET /api/captcha,把 data.image 绑到 <img :src="captcha.image">
  2. 点击图片重新获取(同时清空输入框);
  3. 提交登录时带上 captcha_keycaptcha_code
  4. 登录返回 code === 10021 时,自动重新拉取验证码并提示用户重新输入
  5. 其它失败(如密码错误)也建议刷新验证码,避免用户拿到已消费的 key 反复提交。

四、其它常用接口

4.1 获取当前用户信息

GET /api/userInfo

需登录。返回 uidnicknametokenrolepoints 等字段。

4.2 退出登录

GET /api/logout

五、灰度与上线说明

本文档涉及的两项能力都由后端开关控制,前端可以先行上线,行为与现在完全一致:

能力 后端开关 未开启时的表现
接口签名 CHECK_SIGN 签名头被忽略,按原逻辑放行
登录验证码 CAPTCHA_ENABLED 登录不校验验证码

建议的上线顺序

  1. 后端部署(两个开关均为关闭状态,线上零影响);
  2. 前端发布带签名与验证码的版本;此时功能表现不变,可确认页面无异常;
  3. 后端依次开启开关并清理配置缓存(php artisan config:clear):
    • 先开 CHECK_SIGN,观察日志是否出现验签失败;
    • 再开 CAPTCHA_ENABLED,确认登录流程正常。

需要向后端确认的信息

  • SIGN_SALT 的正式值(需与前端打包配置一致,改动需两端同步);
  • CHECK_SIGNCAPTCHA_ENABLED 的开启时间点;
  • 是否存在验签白名单账号(白名单账号不校验签名,表现为"签名错误也能调用成功",属正常现象,不要据此判断前端实现有误)。

六、变更记录

日期 内容
2026-09-21 初版:接口签名(HMAC-SHA256 + nonce 防重放)、登录图片验证码