|
@@ -1,378 +0,0 @@
|
|
|
-# 前端对接文档
|
|
|
|
|
-
|
|
|
|
|
-> 适用平台:有声合成平台 / 掌维 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 防重放)、登录图片验证码 |
|
|
|