Swift调用Last.fm API POST授权遇阻:Method Signature实现困惑求助
Last.fm iOS Swift API调用:方法签名实现问题
我正在按照Last.fm的移动端认证教程,尝试在iOS应用中用Swift调用其API,但代码存在问题,搞不清楚怎么在程序里实现Last.fm要求的方法签名(method signature)。
以下是我的当前代码:
import UIKit struct LoginRequestBody: Codable { var username: String var password: String var api_key: String var api_sig: String } enum AuthenticationError: Error { case invalidCredentials case custom(errorMessage: String) } class APIService { func requestAPI(username: String, password: String, api_key: String, api_sig: String) { guard let url = URL(string: "http://www.last.fm/api/auth/?api_key=xxx") else { return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" let body = LoginRequestBody(username: username, password: password, api_key: api_key, api_sig: api_sig) let bodyStr = [ "username": "\(body.username)", "password": "\(body.password)", "api_key": "\(body.api_key)", "api_sig": "\(body.api_sig)"] urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: bodyStr, options: .fragmentsAllowed) let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in guard let data = data, error == nil else { return } do { let response = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) print(response) } catch { print(error) } } task.resume() } }
方法签名(Method Signature)实现说明
Last.fm的API签名生成规则是:
- 收集所有请求参数(除了
api_sig本身),按参数名字典序排序 - 将排序后的参数名和值直接拼接成一个连续字符串(比如
api_keyxxxmethodauth.getMobileSessionpasswordmypasswordusernamemyusername) - 在拼接后的字符串末尾追加你的API私钥(shared secret)
- 对整个字符串做MD5哈希运算,得到的十六进制结果就是
api_sig
修改后的完整代码
import UIKit import CryptoKit // iOS 13+ 内置MD5支持,若兼容更低版本需用第三方库 struct LoginRequestBody: Codable { var username: String var password: String var api_key: String var method: String = "auth.getMobileSession" // 移动端认证必填方法 var api_sig: String } enum AuthenticationError: Error { case invalidCredentials case custom(errorMessage: String) } class APIService { // 替换为你的API密钥和私钥 private let apiKey = "你的API_KEY" private let sharedSecret = "你的SHARED_SECRET" func requestMobileSession(username: String, password: String) { // 1. 准备所有参与签名的参数 var parameters: [String: String] = [ "username": username, "password": password, "api_key": apiKey, "method": "auth.getMobileSession" ] // 2. 生成API签名 guard let apiSig = generateAPISignature(parameters: parameters) else { print("签名生成失败") return } parameters["api_sig"] = apiSig // 3. 修正请求URL:使用Last.fm标准API端点 guard let url = URL(string: "https://ws.audioscrobbler.com/2.0/") else { print("无效的API端点URL") return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" // Last.fm API要求表单格式,而非JSON urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") // 4. 将参数转为URL编码的表单数据 urlRequest.httpBody = parameters.percentEncoded() let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in guard let data = data, error == nil else { print(error?.localizedDescription ?? "请求失败") return } do { let response = try JSONSerialization.jsonObject(with: data, options: .allowFragments) print("响应结果:", response) } catch { print("响应解析失败:", error) } } task.resume() } // 生成API签名的核心方法 private func generateAPISignature(parameters: [String: String]) -> String? { // 按参数名字典序排序 let sortedKeys = parameters.keys.sorted() var signatureString = "" // 拼接参数名和对应值 for key in sortedKeys { guard let value = parameters[key] else { continue } signatureString += key + value } // 追加私钥 signatureString += sharedSecret // 计算MD5哈希并转为十六进制字符串 guard let data = signatureString.data(using: .utf8) else { return nil } let md5Hash = Insecure.MD5.hash(data: data) return md5Hash.map { String(format: "%02hhx", $0) }.joined() } } // 扩展:字典转URL编码表单数据 extension [String: String] { func percentEncoded() -> Data? { return map { key, value in let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" return escapedKey + "=" + escapedValue } .joined(separator: "&") .data(using: .utf8) } } extension CharacterSet { static let urlQueryValueAllowed: CharacterSet = { let generalDelimitersToRemove = ":#[]@" let subDelimitersToRemove = "!$&'()*+,;=" var allowed = CharacterSet.urlQueryAllowed allowed.remove(charactersIn: "\(generalDelimitersToRemove)\(subDelimitersToRemove)") return allowed }() }
关键修正点
- 请求端点错误:原代码使用的
/api/auth/不是标准API端点,需替换为https://ws.audioscrobbler.com/2.0/ - 请求格式错误:Last.fm API仅接受
application/x-www-form-urlencoded表单格式,不能用JSON - 签名逻辑缺失:新增
generateAPISignature方法严格遵循Last.fm签名规则 - 参数完整性:必须添加
method=auth.getMobileSession参数,这是移动端认证的必填标识
内容的提问来源于stack exchange,提问作者Mel P. User
相关产品推荐
相关产品推荐

