// // AliyunNLSTokenProvider.swift // suixinkan // import CryptoKit import Foundation /// 阿里云 NLS token 实体,表示语音服务访问凭证及过期时间。 struct AliyunNLSToken: Equatable { let id: String let expireTime: Int64? } /// 阿里云 NLS token 获取协议,便于 TTS 管理器测试替换。 protocol AliyunNLSTokenProviding { /// 使用临时 STS 凭证创建 NLS token。 func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken } /// 阿里云 NLS token Provider,按 OpenAPI POP 签名请求 CreateToken。 final class AliyunNLSTokenProvider: AliyunNLSTokenProviding { typealias DataLoader = (URLRequest) async throws -> Data private let endpoint: URL private let dateProvider: () -> Date private let nonceProvider: () -> String private let dataLoader: DataLoader init( endpoint: URL = URL(string: "https://nls-meta.cn-shanghai.aliyuncs.com/")!, dateProvider: @escaping () -> Date = Date.init, nonceProvider: @escaping () -> String = { UUID().uuidString }, dataLoader: @escaping DataLoader = { request in let (data, response) = try await URLSession.shared.data(for: request) if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) { throw AliyunNLSTokenError.httpStatus(http.statusCode) } return data } ) { self.endpoint = endpoint self.dateProvider = dateProvider self.nonceProvider = nonceProvider self.dataLoader = dataLoader } func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken { var params = signedBaseParams(credentials: credentials) params["Signature"] = Self.signature(params: params, accessKeySecret: credentials.accessKeySecret) let query = Self.canonicalQuery(params) guard let url = URL(string: endpoint.absoluteString + "?" + query) else { throw AliyunNLSTokenError.invalidURL } let request = URLRequest(url: url) let data = try await dataLoader(request) let response = try JSONDecoder().decode(AliyunNLSCreateTokenResponse.self, from: data) guard let id = response.token?.id?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else { throw AliyunNLSTokenError.missingToken } return AliyunNLSToken(id: id, expireTime: response.token?.expireTime) } /// 构造 CreateToken 的未签名公共参数。 func signedBaseParams(credentials: AliyunOSSCredentials) -> [String: String] { [ "AccessKeyId": credentials.accessKeyId, "Action": "CreateToken", "Format": "JSON", "RegionId": "cn-shanghai", "SignatureMethod": "HMAC-SHA1", "SignatureNonce": nonceProvider(), "SignatureVersion": "1.0", "Timestamp": Self.timestampFormatter.string(from: dateProvider()), "Version": "2019-02-28", "SecurityToken": credentials.securityToken, ] } /// 生成阿里云 POP HMAC-SHA1 签名。 static func signature(params: [String: String], accessKeySecret: String) -> String { let canonical = canonicalQuery(params) let stringToSign = "GET&%2F&\(percentEncode(canonical))" let key = SymmetricKey(data: Data("\(accessKeySecret)&".utf8)) let mac = HMAC.authenticationCode(for: Data(stringToSign.utf8), using: key) return Data(mac).base64EncodedString() } /// 生成按参数名排序后的 OpenAPI 查询串。 static func canonicalQuery(_ params: [String: String]) -> String { params .sorted { $0.key < $1.key } .map { "\(percentEncode($0.key))=\(percentEncode($0.value))" } .joined(separator: "&") } /// 阿里云 POP 签名要求的 RFC3986 编码。 static func percentEncode(_ value: String) -> String { var allowed = CharacterSet.alphanumerics allowed.insert(charactersIn: "-_.~") return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value } private static let timestampFormatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime] formatter.timeZone = TimeZone(secondsFromGMT: 0) return formatter }() } /// 阿里云 NLS token 获取错误。 enum AliyunNLSTokenError: Error, Equatable { case invalidURL case httpStatus(Int) case missingToken } private struct AliyunNLSCreateTokenResponse: Decodable { let token: TokenPayload? enum CodingKeys: String, CodingKey { case token = "Token" } struct TokenPayload: Decodable { let id: String? let expireTime: Int64? enum CodingKeys: String, CodingKey { case id = "Id" case expireTime = "ExpireTime" } } }