在 IOS 开发应用开发中使用 WebKit 加载网页资源或者在客户端中需要实现访问应用后端部分,希望采用 http cookie 打通账号系统应该是比较普遍的需求了。通常在后端接口中拿到的 http cookie 都是字符串,但是看苹果的开发文档能发现 WebKit 的 httpCookieStore.setCookie 函数参数是需要一个 HTTPCookie 对象的。

我看了一圈,发现好像居然没人写一个工具函数用于解析 http cookie 字符串…不知道是我搜索方向错了?还是这个需求太简单了大佬们都不愿意写,虽然说确实不难,但是其实我在使用过程中开始还是踩到一个子域共享 Http Cookie 不生效的坑(应该是我太菜了)下文细说,我还是写下来记录一下。

一个常见的 http cookie 字符串如下,这部分的文档可以参考 Using HTTP cookies Cookie specification: RFC 6265

Bin-Auth-Token=43533811-xxxx-xxxx-xxxx-xxxxxxx; Path=/; Domain=bin.zmide.com; Max-Age=604800; HttpOnly

其中,Bin-Auth-Token 就是这个 cookie 的 name 而 43533811-xxxx-xxxx-xxxx-xxxxxxx 就是 value 字段,Domain 是匹配域名,其他字段可以看看上面的文档这里就不一一介绍了。

然后我们看看苹果的文档 HTTPCookie 要通过 Cookie 字符串构建 HTTPCookie 就需要通过 HTTPCookie.init?(properties: [HTTPCookiePropertyKey : Any]) 函数,这个函数只接收一个类型为[HTTPCookiePropertyKey : Any] 字典的 properties 参数,然后我们就是通过解析上面的 cookie 字符串为 [HTTPCookiePropertyKey : Any] 字典,然后用于构建 HTTPCookie 对象。

下面为具体实现函数:

func resolveCookie(cookie: String) -> HTTPCookie? {
    if cookie == "" {
        return nil
    }

    // 解析 cookie 字符串
    var properties: [HTTPCookiePropertyKey: Any] = [
        .secure: "TRUE",
    ]

    // 分割 cookie 字符串
    let cookieList = cookie.components(separatedBy: "; ")
    cookieList.forEach {
        cookieItem in
        // 分割 cookie 项的 key 和 value
        let cookieItemList = cookieItem.components(separatedBy: "=")
        if cookieItemList.count <= 0 {
            return
        }

        let key = cookieItemList[0]
        var value = ""
        if cookieItemList.count == 2 {
            value = cookieItemList[1]
        }

        switch key.lowercased() {
        case HTTPCookiePropertyKey.name.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.name)
        case HTTPCookiePropertyKey.domain.rawValue.lowercased():
            // 如果需要匹配子域需要在域名前增加 . 前缀,如 .bin.zmide.com 用于将该 cookie 作用于 bin.zmide.com 及其全部子域
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.domain)
            // properties.updateValue("." + value, forKey: HTTPCookiePropertyKey.domain)
        case HTTPCookiePropertyKey.path.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.path)
        case HTTPCookiePropertyKey.maximumAge.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.maximumAge)
        case HTTPCookiePropertyKey.originURL.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.originURL)
        case HTTPCookiePropertyKey.version.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.version)
        case HTTPCookiePropertyKey.port.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.port)
        case HTTPCookiePropertyKey.comment.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.comment)
        case HTTPCookiePropertyKey.discard.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.discard)
        case HTTPCookiePropertyKey.secure.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.secure)
        case HTTPCookiePropertyKey.sameSitePolicy.rawValue.lowercased():
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.sameSitePolicy)
        case "HttpOnly".lowercased():
            properties.updateValue(true, forKey: HTTPCookiePropertyKey(rawValue: "HttpOnly"))
        default:
            properties.updateValue(key, forKey: HTTPCookiePropertyKey.name)
            properties.updateValue(value, forKey: HTTPCookiePropertyKey.value)
        }
    }

    return HTTPCookie(properties: properties)
}

在代码中,首先会将 cookie 字符串分割为 cookie 配置项,然后遍历每项,再将 cookie 配置项分割为 key 和 value,最后使用了一个 switch 匹配 key 项,并将值更新到 properties 字典中,最后调用 HTTPCookie init 函数实例化 HTTPCookie 对象。

我遇到的问题就是后端给我返回的 cookie 的 Domain 值为 bin.zmide.com 于是,当我访问 app.bin.zmide.com 时候就会出现没有带上 cookie 的情况,其实正确的做法应该是后端在返回数据时就返回正确的 .bin.zmide.com 要是我自己写后端肯定这样返回,但是和后端商量无果,于是我只能很脏的在代码里自己加前缀了,当然在上面的工具函数中我已经注释了,如果你遇到和我相同的需求也可以尝试取消注释(大概率不会)。

今天的划水文就先到这,如果后续有需要这块代码以后就可以直接拷贝了(Bushi