如何调试iOS中URLSession POST登录认证失败问题
iOS 模拟HTTP Form POST登录遇到419 Page Expired问题调试求助
我在iOS中模拟HTTP Form POST请求实现服务器登录,用Safari/Chrome提交表单能正常登录,已经通过浏览器开发者工具记录了登录所需的GET、POST请求的正确请求头、响应头、Cookie和请求体。但在Xcode运行APP时,POST请求返回419状态码,响应体显示“Page Expired”,推测是Token或Cookie处理不当。现在需要工具或方法,能把APP的GET、POST请求头和请求体和浏览器的请求精准对比,定位问题。
以下是我的代码:
class LoginAPI { public let avLogin = "https://someDomain.com/login" // save response, data from last getHTMLPage() GET fileprivate var lastGetResponse: HTTPURLResponse? = nil fileprivate var lastGetData: String? = nil // test POST with saved values var loginToken = "" var cookies: [HTTPCookie] = [] // MARK: Login func login() async -> String { // GET login page, let loginGetHTML = await self.getHTMLPage(url: self.avLogin) let loginToken = self.scrapeLoginToken(html: loginGetHTML) let cookies = self.getCookiesFromResponse(response: self.lastGetResponse) if let lastResponse = self.lastGetResponse, let lastURL = lastResponse.url { HTTPCookieStorage.shared.setCookies(cookies, for: lastURL, mainDocumentURL: nil) } // allow testing of Login, then POST self.loginToken = loginToken self.cookies = cookies // TO DO: add delay, then call loginPost(), and return Data as String return "" } // MARK: POST Login form func loginPost(url: String, loginToken: String, cookies: [HTTPCookie]) async { guard let loginURL = URL(string: url) else {return} let email = "fake123@gmail.com" let password = "pass123" var request = URLRequest(url: loginURL) request.httpMethod = "POST" request.url = loginURL // header request.httpShouldHandleCookies = true // body let loginInfo = [ ("_token" , loginToken), ("email" , email), ("password", password) ] let body = urlEncode(loginInfo) request.httpBody = Data(body.utf8) let session = URLSession.shared session.configuration.httpCookieStorage = HTTPCookieStorage.shared session.configuration.httpCookieAcceptPolicy = .always session.configuration.httpShouldSetCookies = true let task = session.dataTask(with: request) { (data, response, error) in if let error = error { print ("POST error: \(error)") } guard let response = response as? HTTPURLResponse else { print("invalid POST response") return } print("response") let statusCode = response.statusCode let headerFields = response.allHeaderFields let cookies = headerFields["Set-Cookie"] // let cookie = response.value(forKey: "Set-Cookie") print(" status code = \(statusCode)") print(" cookies = \(cookies.debugDescription)") print(response) if let mimeType = response.mimeType, let data = data, let page = String(data: data, encoding: .utf8) { print("mimeType \(mimeType)") print("page as UTF-8") print(page) } } task.resume() } // MARK: GET public func getHTMLPage(url urlString: String) async -> String { var statusCode = 0 // HTTP Response status code // void prior cached response, data self.lastGetResponse = nil self.lastGetData = nil guard let url = URL(string: urlString) else { print("Error: Invalid URL: '\(urlString)'") return "" } do { let (data, response) = try await URLSession.shared.data(from: url) if let httpResponse = response as? HTTPURLResponse { statusCode = httpResponse.statusCode self.lastGetResponse = httpResponse print("GET response") print(response) } else { print("Error: couldn't get HTTP Response") return "" } guard statusCode == 200 else { print("Error: Bad HTTP status code. code=\(statusCode)") return "" } let page = String(decoding: data, as: UTF8.self) self.lastGetData = page return page } catch { print("Error: catch triggerred") return "" } } // MARK: Login Helper Functions private func getCookiesFromResponse(response: HTTPURLResponse?) -> [HTTPCookie] { guard let response = response, let responseURL = response.url else { return [] } guard let responseHeaderFields = response.allHeaderFields as? [String : String] else { return [] } let cookies = HTTPCookie.cookies( withResponseHeaderFields: responseHeaderFields, for: responseURL) return cookies } // MARK: Login token public func scrapeLoginToken(html: String) -> String { look for name="_token", value="40-char-string" return <40-char-string } // MARK: Login urlEncode public func urlEncode(_ params: [(String, String)]) -> String { var paramArray: [String] = [] for param in params { let (name, value) = param let valueEnc = urlEncode(value) paramArray.append("\(name)=\(valueEnc)") } let body = paramArray.joined(separator: "&") return body } private func urlEncode(_ string: String) -> String { let allowedCharacters = CharacterSet.alphanumerics return string.addingPercentEncoding( withAllowedCharacters: allowedCharacters) ?? "" } }
调试方向与工具建议
一、请求对比工具
- Charles Proxy:将APP的流量代理到Charles,同时捕获浏览器的请求,直接在界面中对比两者的请求头、Cookie、请求体的每一项差异。需要在iOS设备/模拟器上配置代理,确保APP请求经过Charles。
- Xcode Network Inspector:在Xcode的Debug Navigator中打开Network面板,查看APP发送的所有请求详情,导出请求为HAR格式;同时在浏览器开发者工具中导出请求的HAR文件,用HAR对比工具进行差异对比。
二、代码层面潜在问题排查
- Cookie处理:
- 检查
HTTPCookieStorage.shared是否正确保存了GET请求返回的所有Cookie,包括框架专属Cookie(如Laravel的laravel_session,419常见于CSRF验证失败,需要会话Cookie匹配)。 loginPost中无需手动设置session.configuration.httpCookieStorage,URLSession.shared默认会使用共享Cookie存储,确保请求自动带上之前保存的Cookie。
- 检查
- CSRF Token验证:
- 确认
scrapeLoginToken方法正确提取页面中的_token值,注意区分大小写,以及是否存在多个_token元素的情况。 - 检查POST请求的
_token参数是否和GET页面返回的完全一致,无编码错误。
- 确认
- 请求头完整性:
- 必须添加
Content-Type: application/x-www-form-urlencoded请求头,浏览器提交表单时会自动带上该头,缺少它会导致服务器无法正确解析请求体,这很可能是当前问题的核心原因。 - 检查是否需要添加
User-Agent头,模拟浏览器UA,部分服务器会验证该字段。
- 必须添加
- URL编码问题:
- 当前
urlEncode方法仅允许字母数字,会过度编码特殊字符(如@、.),建议使用CharacterSet.urlQueryAllowed替代alphanumerics,匹配浏览器的编码规则。
- 当前
三、临时调试技巧
- 在
loginPost中添加打印语句,输出完整请求头和请求体,与浏览器内容逐行对比:print("Request Headers: \(request.allHTTPHeaderFields ?? [:])") print("Request Body: \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "")") - 直接复制浏览器的POST请求体作为APP的请求体发送,判断是请求体问题还是Cookie/Token问题。
内容的提问来源于stack exchange,提问作者SDGary
相关产品推荐
相关产品推荐

