@Heebeom Kim

WKWebView! Where is my session cookie?

WKWebView를 다루면서 가장 많은 시간을 투자하는 부분이 세션 쿠키와 관련된 것이었다. 이번에 새로운 프로젝트를 진행하면서도 역시나 마찬가지로 세션 쿠키와 관련된 문제가 꾸준히 있었다.

웹뷰의 동작은 단순하지만 생각치못한 단순한 문제들로 인해 많은 시행착오를 겪게 된다. 의도치 않은 리로드가 되기도 하고 세션 쿠키가 사라져서 자주 로그인 페이지로 리다이렉트되기도 한다. 이 포스트에서는 웹뷰에서 세션 쿠키를 유지하기 위한 두 가지 전략을 소개하고자 한다.

쿠키 주입 방식

우리 프로젝트는 웹뷰 기반이었고, 당연히 로그인 페이지로 웹으로 되어있다. 유저가 로그인 페이지에서 로그인을 하고나면 세션 쿠키가 생성되고 그 값으로 사용자 인증을 거치게 된다. 하지만 앱에서는 API요청을 보내기도 하는데, 그 때 웹뷰에 있는 세션 쿠키가 필요하다. 그렇기에 세션 쿠키를 스토리지에 저장해야 했다.

  func storeCookies() {
    webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
      Storage.shared.saveCookies(cookies)
    }
  }

iOS 11 부터는 WKHTTPCookieStore에서 간단하게 쿠키를 가져올 수 있다. 나는 navigationResponse가 불리는 시점에 항상 웹뷰에 있는 쿠키를 로컬 스토리지로 저장하는 로직을 추가하였다.

  func webView(
    _ webView: WKWebView, 
    decidePolicyFor navigationResponse: WKNavigationResponse,
    decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
  ) {
    guard let url = navigationResponse.response.url else {
      decisionHandler(.cancel)
      return
    }

    storeCookies()

    ...
  }

위에서처럼 페이지 이동이 발생할 때 매번 쿠키를 스토리지에 저장하면 API를 요청할 때 세션 쿠키가 필요한 문제는 해결할 수 있다.

하지만 여기까지만 작업을 했을 때는 웹뷰의 쿠키가 앱의 한 생애주기 동안만 유효하였고, 앱을 종료했다가 실행하면 세션 쿠키가 날라가서 매번 로그인 페이지로 이동하게 되었다. 그래서 아래와 같은 쿠키를 주입하는 코드를 추가하였다.

  func injectBoeanCookie() {
    if let cookie = Storage.shared.getCookie() {
      webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
    }
  }

페이지 요청이 되기 전에 명시적으로 쿠키를 주입하기 때문에 문제를 해결할 수 있다.

  func webView(
    _ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  ) {

    injectSessionCookie()

    ...
  }

expiresAt 사용

웹뷰에서 쿠키의 expiresAt이 지났거나 null이면 앱을 새로 실행했을 때 쿠키가 삭제된다는 것을 알았다면 아마 앞선 방법을 사용하지 않았을 것이다.

print("WebCookie Test: App Start")
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
configuration.websiteDataStore = WKWebsiteDataStore.default()
let webview = WKWebView(frame: .init(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height), configuration: configuration)

configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
  if let cookie = cookies.first(where: { $0.name == "session" }) {
    print("WebCookie Test: Cookie storage have cookie name: \(cookie.name), value: \(cookie.value), expiresDate: \(String(describing: cookie.expiresDate))")
  } else {
    print("WebCookie Test: Cookie storage doesn't have cookie, it'll make a new cookie")
    guard let sessionCookie = HTTPCookie(properties: [.domain: ".mydomain.com", .path: "/", .name: "session", .value: "test"]) else { return }
    configuration.websiteDataStore.httpCookieStore.setCookie(sessionCookie) {
      configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
        if let cookie = cookies.first(where: { $0.name == "session" }) {
          print("WebCookie Test: Cookie storage updated name: \(cookie.name), value: \(cookie.value), expiresDate: \(String(describing: cookie.expiresDate))")
        }
      }
    }
  }
}

위 테스트 코드에서 우리는 별도의 expiresDate를 지정해주지 않았다.

let sessionCookie = HTTPCookie(properties: [.domain: ".mydomain.com", .path: "/", .name: "session", .value: "test"])

결론적으로 매 실행시 마다 항상 새로운 쿠키가 생성되는 것을 확인할 수 있었다.

WebCookie: App Start
WebCookie: Cookie storage doesn't have cookie, it'll make a new cookie
WebCookie: Cookie storage updated name: session, value: test, expiresDate: nil

두 번째 테스트는 동일한 상황에서 쿠키의 expiresDate를 다음날로 지정하여 테스트를 하였다.

let tomorrow = Date().addingTimeInterval(3600)
let sessionCookie = HTTPCookie(properties: [.domain: ".mydomain.com", .path: "/", .name: "session", .value: "test", .expires: tomorrow])

그 결과 실행할 때마다 쿠키가 새로 생성되는 것이 아닌, 최초 실행 시에만 쿠키를 생성하고 이후 실행시에는 기존의 쿠키가 유지되어 새로운 쿠키가 생성되지 않았다.

최초 실행
WebCookie: App Start
WebCookie: Cookie storage doesn't have cookie, it'll make a new cookie
WebCookie: Cookie storage updated name: session, value: test, expiresDate: Optional(2022-04-29 08:57:10 +0000)
재실행
WebCookie: App Start
WebCookie: Cookie storage have cookie name: session, value: test, expiresDate: Optional(2022-04-29 08:57:10 +0000)

Written by@Heebeom Kim
iOS Engineer. Interested in Architecture, Automation. Work for CPNG

GitHubLinkedIn