|
|
@@ -0,0 +1,378 @@
|
|
|
+# 前端对接文档
|
|
|
+
|
|
|
+> 适用平台:有声合成平台 / 掌维 AI 短剧
|
|
|
+> 后端:Laravel,接口统一前缀 `/api`
|
|
|
+> 本文档覆盖前端需要改造对接的两块内容:**接口签名**与**登录图片验证码**,以及通用约定与错误码。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、通用约定
|
|
|
+
|
|
|
+### 1.1 接口地址与鉴权
|
|
|
+
|
|
|
+- 所有业务接口以 `/api` 开头,例如 `POST /api/anime/detail`。
|
|
|
+- 登录后,除少量免登录接口外,均需在请求头携带登录令牌:
|
|
|
+
|
|
|
+ ```
|
|
|
+ d-token: <登录接口返回的 token>
|
|
|
+ ```
|
|
|
+- 少数导出类接口服务端也兼容从参数 `d_token` 读取令牌,**新代码请统一使用请求头**。
|
|
|
+- 免登录接口(不需要 `d-token`):`GET /api/login`、`GET /api/captcha`、`ANY /api/video/seedanceCallback`。
|
|
|
+
|
|
|
+### 1.2 响应格式
|
|
|
+
|
|
|
+成功:
|
|
|
+
|
|
|
+```json
|
|
|
+{ "msg": "", "code": 0, "data": { } }
|
|
|
+```
|
|
|
+
|
|
|
+失败(业务异常):
|
|
|
+
|
|
|
+```json
|
|
|
+{ "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/*`(`scriptList`、`scriptInfo`、`saveEpisodeContent` 等)
|
|
|
+- 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 跑一遍验证:
|
|
|
+
|
|
|
+```js
|
|
|
+// 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` 与真实请求对齐。
|
|
|
+
|
|
|
+### 2.4 前端实现
|
|
|
+
|
|
|
+#### 方案 A:使用 crypto-js(推荐,改动最小)
|
|
|
+
|
|
|
+```bash
|
|
|
+npm install crypto-js
|
|
|
+```
|
|
|
+
|
|
|
+```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(不新增依赖)
|
|
|
+
|
|
|
+```js
|
|
|
+// 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 拦截器
|
|
|
+
|
|
|
+```js
|
|
|
+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` 对不上 | 注意`axios` 的 `baseURL`:若 `/api` 写在 `baseURL` 里、`url` 只写了 `/anime/detail`,需手动补成 `/api/anime/detail` |
|
|
|
+| token 对不上 | 签名里的`d-token` 必须与请求头 `d-token` 完全一致(含大小写),若 token 做过 trim 或转换需保持一致 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、登录图片验证码
|
|
|
+
|
|
|
+### 3.1 获取验证码
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/captcha
|
|
|
+```
|
|
|
+
|
|
|
+免登录,无需签名。返回:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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` | 后端开启验证码时必填 | 用户输入的验证码,大小写不敏感 |
|
|
|
+
|
|
|
+成功返回:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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_key` 与 `captcha_code`;
|
|
|
+4. **登录返回 `code === 10021` 时,自动重新拉取验证码并提示用户重新输入**;
|
|
|
+5. 其它失败(如密码错误)也建议刷新验证码,避免用户拿到已消费的 `key` 反复提交。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、其它常用接口
|
|
|
+
|
|
|
+### 4.1 获取当前用户信息
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/userInfo
|
|
|
+```
|
|
|
+
|
|
|
+需登录。返回 `uid`、`nickname`、`token`、`role`、`points` 等字段。
|
|
|
+
|
|
|
+### 4.2 退出登录
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/logout
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、灰度与上线说明
|
|
|
+
|
|
|
+本文档涉及的两项能力都由后端开关控制,前端可以先行上线,行为与现在完全一致:
|
|
|
+
|
|
|
+| 能力 | 后端开关 | 未开启时的表现 |
|
|
|
+| ---------- | ------------------- | -------------------------- |
|
|
|
+| 接口签名 | `CHECK_SIGN` | 签名头被忽略,按原逻辑放行 |
|
|
|
+| 登录验证码 | `CAPTCHA_ENABLED` | 登录不校验验证码 |
|
|
|
+
|
|
|
+**建议的上线顺序**
|
|
|
+
|
|
|
+1. 后端部署(两个开关均为关闭状态,线上零影响);
|
|
|
+2. 前端发布带签名与验证码的版本;此时功能表现不变,可确认页面无异常;
|
|
|
+3. 后端依次开启开关并清理配置缓存(`php artisan config:clear`):
|
|
|
+ - 先开 `CHECK_SIGN`,观察日志是否出现验签失败;
|
|
|
+ - 再开 `CAPTCHA_ENABLED`,确认登录流程正常。
|
|
|
+
|
|
|
+**需要向后端确认的信息**
|
|
|
+
|
|
|
+- `SIGN_SALT` 的正式值(需与前端打包配置一致,改动需两端同步);
|
|
|
+- `CHECK_SIGN` 与 `CAPTCHA_ENABLED` 的开启时间点;
|
|
|
+- 是否存在验签白名单账号(白名单账号不校验签名,表现为"签名错误也能调用成功",属正常现象,不要据此判断前端实现有误)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、变更记录
|
|
|
+
|
|
|
+| 日期 | 内容 |
|
|
|
+| ---------- | ------------------------------------------------------------ |
|
|
|
+| 2026-09-21 | 初版:接口签名(HMAC-SHA256 + nonce 防重放)、登录图片验证码 |
|