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

如何调试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对比工具进行差异对比。

二、代码层面潜在问题排查

  1. Cookie处理:
    • 检查HTTPCookieStorage.shared是否正确保存了GET请求返回的所有Cookie,包括框架专属Cookie(如Laravel的laravel_session,419常见于CSRF验证失败,需要会话Cookie匹配)。
    • loginPost中无需手动设置session.configuration.httpCookieStorage,URLSession.shared默认会使用共享Cookie存储,确保请求自动带上之前保存的Cookie。
  2. CSRF Token验证:
    • 确认scrapeLoginToken方法正确提取页面中的_token值,注意区分大小写,以及是否存在多个_token元素的情况。
    • 检查POST请求的_token参数是否和GET页面返回的完全一致,无编码错误。
  3. 请求头完整性:
    • 必须添加Content-Type: application/x-www-form-urlencoded请求头,浏览器提交表单时会自动带上该头,缺少它会导致服务器无法正确解析请求体,这很可能是当前问题的核心原因。
    • 检查是否需要添加User-Agent头,模拟浏览器UA,部分服务器会验证该字段。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 00:00:58