3.B 修复:七牛 token 401 bad token(base64URL padding + scope 字段)

## 问题

录音 + 转码正常,但七牛上传报:
  status: 401 / error: bad token / QiniuUploadError error 1

## 根因

逐字段对照 daoqi QiniuManager.m:202-236 generateUploadToken,发现两处
关键不一致:

### 1. base64URL padding:新外壳删了 `=`,daoqi 保留

daoqi urlsafeBase64EncodeData:240-245 只替换 `+→-` 和 `/→_`,**保留
末尾 `=` padding**。

新外壳额外做了 `=→空` 替换,本意符合 RFC 4648 §5 base64URL 的可选
padding 删除,但七牛后端 decode token 时**依赖 padding 还原 base64**,
删 `=` 会让 HMAC 验签数据流不一致 → 401 bad token。

### 2. scope 字段:新外壳带了 `:key` 后缀,daoqi 只用 bucket

daoqi QiniuManager.m:207 putPolicy.scope = bucketName(限定 bucket,
允许任意 key)。

新外壳之前传 key 参数后构造 `bucket:key`(限定 key 精确匹配),与
daoqi 行为不一致。带 `:key` 后缀也是七牛合法 scope,但七牛后端
校验更严,且无必要——key 已经在 putFile API 单独传递,token 内不必
重复声明。

## 修复

- base64URLEncode:移除 `=→空` 替换
- uploadToken(key:):key 参数标记为废弃但保留(避免破坏调用方),
  putPolicy.scope 改为只用 bucket
- 顺带:JSONSerialization 不再强制 sortedKeys(daoqi 用 options:0)

## 验证

BuildProject 通过 0 错误。真机重试录音上传应不再 401。
This commit is contained in:
joywayer
2026-06-24 02:09:28 +08:00
parent 7d89a9d3ee
commit c3a0ddbf8b
@@ -22,25 +22,24 @@ public enum QiniuTokenSigner {
/// 生成上传 token。 /// 生成上传 token。
/// - Parameters: /// - Parameters:
/// - key: 上传文件 key(可空:scope = bucket,allow override;非空:scope = bucket:key) /// - key: 已废弃参数;保留签名兼容性,但不再写入 putPolicy。daoqi
/// QiniuManager.m:207 的 putPolicy.scope 仅含 bucket(限定 bucket、
/// 允许任意 key),与新外壳上传 key 是分离的两个维度。带 `:key` 后
/// 缀(限定 key 精确匹配)会让七牛后端校验更严,且与 daoqi 行为不一致
/// - expiresIn: token 有效时长(默认 3600 秒) /// - expiresIn: token 有效时长(默认 3600 秒)
public nonisolated static func uploadToken(key: String? = nil, expiresIn: TimeInterval = 3600) -> String { public nonisolated static func uploadToken(key: String? = nil, expiresIn: TimeInterval = 3600) -> String {
// 1. 拼 scope _ = key // 参数保留以避免破坏调用方,但不再使用
let scope: String
if let key, !key.isEmpty { // 1. scope 仅含 bucket(对齐 daoqi QiniuManager.m:207)
scope = "\(QiniuConfig.bucketName):\(key)" let scope = QiniuConfig.bucketName
} else {
scope = QiniuConfig.bucketName
}
let deadline = Int(Date().timeIntervalSince1970 + expiresIn) let deadline = Int(Date().timeIntervalSince1970 + expiresIn)
// 2. putPolicy JSON(注意 key 顺序与 msext 等价:scope / deadline, // 2. putPolicy JSON(daoqi 用默认 options:0,不强制排序)
// 实际七牛后端不要求顺序,但保持与 msext 一致便于排错)
let putPolicy: [String: Any] = [ let putPolicy: [String: Any] = [
"scope": scope, "scope": scope,
"deadline": deadline "deadline": deadline
] ]
let policyData = (try? JSONSerialization.data(withJSONObject: putPolicy, options: [.sortedKeys])) ?? Data() let policyData = (try? JSONSerialization.data(withJSONObject: putPolicy, options: [])) ?? Data()
// 3. Base64URL(putPolicy) // 3. Base64URL(putPolicy)
let encodedPolicy = base64URLEncode(policyData) let encodedPolicy = base64URLEncode(policyData)
@@ -58,12 +57,12 @@ public enum QiniuTokenSigner {
return "\(QiniuConfig.accessKey):\(encodedSign):\(encodedPolicy)" return "\(QiniuConfig.accessKey):\(encodedSign):\(encodedPolicy)"
} }
/// Base64URL: 标准 Base64 把 + → -、/ → _、去掉末尾 = /// Base64URL: 标准 Base64 把 + → -、/ → _,**保留末尾 `=` padding**。
/// 与七牛 SDK `QNUrlSafeBase64.encodeData:` 等价 /// 对齐 daoqi QiniuManager.m:240-245 `urlsafeBase64EncodeData:`,七牛后端
/// decode token 时依赖 padding 还原 base64;删 `=` 会触发 401 bad token。
private nonisolated static func base64URLEncode(_ data: Data) -> String { private nonisolated static func base64URLEncode(_ data: Data) -> String {
data.base64EncodedString() data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
} }
} }