You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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签名生成规则是:

  1. 收集所有请求参数(除了api_sig本身),按参数名字典序排序
  2. 将排序后的参数名和值直接拼接成一个连续字符串(比如api_keyxxxmethodauth.getMobileSessionpasswordmypasswordusernamemyusername)
  3. 在拼接后的字符串末尾追加你的API私钥(shared secret)
  4. 对整个字符串做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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 06:10:39