Swift 签名 SDK 接入指南
功能说明
Swift 签名 SDK 用于为美图开放平台 API 请求生成以下认证信息:
X-Sdk-Date:UTC 请求时间。Authorization:基于 AK/SK 和请求内容生成的 HMAC-SHA256 签名。
SDK 返回包含 HTTP 方法、请求头和 body 的 URLRequest,可直接通过 URLSession 发送。
SK 只能保存在服务端,不可写入会发布给用户的 Apple 客户端。
环境要求
- Apple 平台 Swift 工程。
- 完整示例使用 Swift 5.5 或更高版本、macOS 12 或更高版本。
- 依赖
Foundation和CommonCrypto;SDK 1.0.0 不使用 CryptoKit。 - 服务端系统时间需要保持准确,签名时间使用 UTC。
下载与引入
解压后包含:
AIGCP-API-swift-sdk-1.0.0/
├── MTSigner.swift
└── MTDemo.swift将 MTSigner.swift 加入服务端 Swift target,并确认该文件属于正确的 Target Membership。MTDemo.swift 是独立 Demo 入口,接入已有工程时无需引入。
接口说明
SDK 的公开常量和方法如下:
public let kBasicDateFormat = "yyyyMMdd'T'HHmmss'Z'"
public let kAlgorithm = "SDK-HMAC-SHA256"
public let kHeaderXDate = "X-Sdk-Date"
public let kHeaderHost = "Host"
public let kHeaderAuthorization = "Authorization"
public let kHeaderContentSha256 = "X-Sdk-Content-Sha256"
public class MTSigner {
public init(key: String, secret: String)
public func signRequest(
url: URL,
method: String,
headers: [String: String],
body: String
) throws -> URLRequest
}| 参数 | 说明 |
|---|---|
key | 应用的 AK |
secret | 与 AK 配套的 SK |
url | 最终请求 URL,包含已编码并排序的查询参数 |
method | HTTP 方法,使用接口要求的大写值,如 GET、POST |
headers | 参与签名的请求头;必须包含 Host |
body | 实际发送的 UTF-8 字符串;GET 请求传空字符串 |
| 返回值 | 已添加 X-Sdk-Date、Authorization 和请求 body 的 URLRequest |
若 headers 已包含 X-Sdk-Date,SDK 使用该值;否则自动生成 UTC 时间。传入的所有请求头都会参与签名,签名完成后不得更改。
POST 完整调用示例
以下示例调用正式同步任务提交地址:
https://openapi.meitu.com/api/v1/sdk/sync/push
运行前设置 MEITU_OPENAPI_AK、MEITU_OPENAPI_SK、MEITU_TASK 和 MEITU_IMAGE_URL。MEITU_TASK、图片结构、params、task_type 和 sync_timeout 的取值以目标能力 API 文档为准。
请求 body 包含且仅包含五个顶层字段:task、task_type、init_images、params 和 sync_timeout。其中 params 必须是 JSON 字符串。
import Foundation
enum IntegrationError: LocalizedError {
case missingEnvironment(String)
case invalidURL(String)
case invalidUTF8Body
case invalidHTTPResponse
var errorDescription: String? {
switch self {
case .missingEnvironment(let name):
return "Missing environment variable: \(name)"
case .invalidURL(let value):
return "Invalid URL: \(value)"
case .invalidUTF8Body:
return "Failed to create a stable UTF-8 request body"
case .invalidHTTPResponse:
return "The server did not return an HTTP response"
}
}
}
func requiredEnvironment(_ name: String) throws -> String {
guard let value = ProcessInfo.processInfo.environment[name], !value.isEmpty else {
throw IntegrationError.missingEnvironment(name)
}
return value
}
@main
struct PostExample {
static func main() async {
do {
let accessKey = try requiredEnvironment("MEITU_OPENAPI_AK")
let secretKey = try requiredEnvironment("MEITU_OPENAPI_SK")
let taskName = try requiredEnvironment("MEITU_TASK")
let imageURL = try requiredEnvironment("MEITU_IMAGE_URL")
guard URL(string: imageURL) != nil else {
throw IntegrationError.invalidURL(imageURL)
}
let endpoint = "https://openapi.meitu.com/api/v1/sdk/sync/push"
guard let url = URL(string: endpoint) else {
throw IntegrationError.invalidURL(endpoint)
}
let paramsObject: [String: Any] = [
"parameter": ["rsp_media_type": "url"]
]
let paramsData = try JSONSerialization.data(withJSONObject: paramsObject)
guard let params = String(data: paramsData, encoding: .utf8) else {
throw IntegrationError.invalidUTF8Body
}
let payload: [String: Any] = [
"task": taskName,
"task_type": "formula",
"init_images": [[
"url": imageURL,
"profile": [
"media_profiles": ["media_data_type": "url"],
"version": "v1"
]
]],
"params": params,
"sync_timeout": 30
]
let bodyData = try JSONSerialization.data(withJSONObject: payload)
guard let body = String(data: bodyData, encoding: .utf8),
body.data(using: .utf8) == bodyData else {
throw IntegrationError.invalidUTF8Body
}
let headers: [String: String] = [
kHeaderHost: "openapi.meitu.com",
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json"
]
let signer = MTSigner(key: accessKey, secret: secretKey)
let request = try signer.signRequest(
url: url,
method: "POST",
headers: headers,
body: body
)
guard request.httpBody == bodyData else {
throw IntegrationError.invalidUTF8Body
}
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 60
let session = URLSession(configuration: configuration)
let (responseData, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw IntegrationError.invalidHTTPResponse
}
let responseBody = String(data: responseData, encoding: .utf8) ?? ""
print("HTTP \(httpResponse.statusCode)")
print(responseBody)
} catch {
print("Request failed: \(error.localizedDescription)")
}
}
}代码只序列化一次业务 payload,并直接发送签名器返回的 URLRequest。因此参与签名和实际发送的是同一份 UTF-8 body Data。
GET 调用示例
GET 请求传空 body。urlString 必须替换为目标接口的最终 HTTPS URL;若包含多个查询参数,应在签名前完成 RFC 3986 编码和排序。
import Foundation
enum GETExampleError: Error {
case invalidURL
case invalidHTTPResponse
}
func callSignedGET(
accessKey: String,
secretKey: String,
urlString: String
) async throws -> (statusCode: Int, body: Data) {
guard let url = URL(string: urlString),
url.scheme == "https",
url.host == "openapi.meitu.com" else {
throw GETExampleError.invalidURL
}
let headers: [String: String] = [
kHeaderHost: "openapi.meitu.com",
"Accept": "application/json"
]
let signer = MTSigner(key: accessKey, secret: secretKey)
let request = try signer.signRequest(
url: url,
method: "GET",
headers: headers,
body: ""
)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw GETExampleError.invalidHTTPResponse
}
return (httpResponse.statusCode, data)
}注意事项
- 使用 HTTPS,并把
Host设置为openapi.meitu.com。 - 方法、URL、查询串、已签请求头和 body 在签名后必须保持不变;直接发送
signRequest返回的请求。 - JSON 只能序列化一次。不要在签名后调整字段、空格、换行或字符编码。
- 查询参数需要先按接口签名规范完成百分比编码和排序,再构造最终 URL。
X-Sdk-Date必须是 UTCyyyyMMdd'T'HHmmss'Z',并保持服务端时钟同步。- 默认对 UTF-8 body 计算 SHA-256。只有目标接口明确支持时,才可在签名前加入:
var headers: [String: String] = [
kHeaderHost: "openapi.meitu.com",
"Content-Type": "application/json; charset=utf-8",
kHeaderContentSha256: "UNSIGNED-PAYLOAD"
]- AK/SK 认证请求的 body 应控制在 12 MB 以内。
常见错误
| 错误现象 | 处理方式 |
|---|---|
no such module 'CommonCrypto' | 使用 Apple 平台 Xcode/Swift 工具链,并确认 MTSigner.swift 属于正确 target |
401、403 或签名校验失败 | 检查 AK/SK、服务端时间、HTTP 方法、Host、最终 URL、查询串、请求头和 body 是否一致 |
| POST 验签失败 | 确认只序列化一次 JSON,并直接发送 SDK 返回的 URLRequest |
| GET 验签失败 | 确认查询参数已编码和排序,且签名后没有修改 URL |
UNSIGNED-PAYLOAD 不生效 | 确认目标接口支持该方式,并使用精确的请求头名称和值 |
| 请求超时 | 根据目标能力文档设置 sync_timeout,同时保证 URLSession 超时大于接口处理时间 |
| 返回非 2xx | 记录脱敏后的状态码和响应 body,并按目标能力 API 文档中的业务错误码处理 |