You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何配置代理解决GCDWebServer+WKWebView的跨域请求问题?

用GCDWebServer搭建反向代理解决WKWebView跨域+Cookie问题

刚好我之前也踩过GCDWebServer代理+WKWebView跨域+Cookie的坑,给你一套亲测有效的解决方案——核心思路是用GCDWebServer创建反向代理端点,让本地网页的请求先发给代理,由代理转发到后端服务器,同时处理跨域头和Cookie同步,完美绕开浏览器的跨域限制。

实现步骤

1. 初始化GCDWebServer并添加代理路由

首先创建一个代理服务器类,给GCDWebServer添加对应HTTP方法的路由(GET/POST/PUT/DELETE/OPTIONS都要覆盖,尤其是OPTIONS的预检请求,浏览器会自动发起)。

2. 编写请求转发逻辑

代理的核心是把本地请求转发到后端,同时处理Cookie和跨域头:

  • 提取目标后端URL:把请求路径中/proxy/后面的部分拼接成完整的后端接口地址
  • 复制原请求的headers、body到新的请求中,特别要带上后端的Cookie
  • 转发请求后,给本地网页返回响应,并添加CORS允许头
  • 同步后端返回的Set-Cookie到WKWebView的Cookie存储中

3. 修改本地网页的请求地址

把网页里原本直接请求后端的URL,替换成GCDWebServer的代理地址,比如原请求https://your-backend.com/api/user改成http://localhost:8080/proxy/api/user(假设GCDWebServer跑在8080端口)。

完整代码示例(Swift)

import GCDWebServer
import WebKit

class LocalProxyServer {
    let webServer = GCDWebServer()
    // 替换成你的后端基础地址
    let backendBaseURL = "https://your-backend-domain.com"
    // 本地服务器地址,要和CORS头里的源一致
    let localServerURL = "http://localhost:8080"
    
    init() {
        setupProxyHandlers()
        startServer()
    }
    
    private func setupProxyHandlers() {
        // 处理GET请求
        webServer.addHandlerForMethod("GET", path: "/proxy/*", requestClass: GCDWebServerRequest.self) { [weak self] request in
            return self?.handleProxyRequest(request)
        }
        
        // 处理POST请求
        webServer.addHandlerForMethod("POST", path: "/proxy/*", requestClass: GCDWebServerRequest.self) { [weak self] request in
            return self?.handleProxyRequest(request)
        }
        
        // 处理PUT/DELETE等其他方法,按需添加
        webServer.addHandlerForMethod("PUT", path: "/proxy/*", requestClass: GCDWebServerRequest.self) { [weak self] request in
            return self?.handleProxyRequest(request)
        }
        
        // 处理OPTIONS预检请求(必须加,否则浏览器会拦截跨域请求)
        webServer.addHandlerForMethod("OPTIONS", path: "/proxy/*", requestClass: GCDWebServerRequest.self) { [weak self] _ in
            guard let self = self else { return GCDWebServerResponse(statusCode: 500) }
            
            let response = GCDWebServerResponse(statusCode: 200)
            // 允许本地服务器跨域请求
            response?.setValue(self.localServerURL, forAdditionalHeader: "Access-Control-Allow-Origin")
            // 允许携带Cookie
            response?.setValue("true", forAdditionalHeader: "Access-Control-Allow-Credentials")
            // 允许的HTTP方法
            response?.setValue("GET, POST, PUT, DELETE, OPTIONS", forAdditionalHeader: "Access-Control-Allow-Methods")
            // 允许的请求头
            response?.setValue("Content-Type, Authorization, Cookie", forAdditionalHeader: "Access-Control-Allow-Headers")
            // 预检缓存时长,减少OPTIONS请求次数
            response?.setValue("86400", forAdditionalHeader: "Access-Control-Max-Age")
            return response
        }
    }
    
    private func handleProxyRequest(_ request: GCDWebServerRequest) -> GCDWebServerResponse? {
        guard let self = self else { return GCDWebServerResponse(statusCode: 500) }
        
        // 提取代理路径,去掉开头的"/proxy/"
        let proxyPath = request.path.replacingOccurrences(of: "/proxy/", with: "")
        guard let targetURL = URL(string: "\(self.backendBaseURL)/\(proxyPath)") else {
            print("Invalid target URL")
            return GCDWebServerResponse(statusCode: 400)
        }
        
        // 构建转发请求
        var urlRequest = URLRequest(url: targetURL)
        urlRequest.httpMethod = request.method
        
        // 复制原请求的headers(跳过Host,由目标URL自动处理)
        for (key, value) in request.headers {
            if key.lowercased() != "host" {
                urlRequest.setValue(value, forHTTPHeaderField: key)
            }
        }
        
        // 添加后端Cookie:从系统Cookie存储和WKWebView的Cookie存储中同步
        var cookies = [HTTPCookie]()
        if let systemCookies = HTTPCookieStorage.shared.cookies(for: targetURL) {
            cookies.append(contentsOf: systemCookies)
        }
        // 同步WKWebView的Cookie(如果有单独存储的话)
        WKHTTPCookieStore.default.getAllCookies { wkCookies in
            cookies.append(contentsOf: wkCookies.filter { $0.domain == targetURL.host })
        }
        let cookieHeader = HTTPCookie.requestHeaderFields(with: cookies)
        urlRequest.allHTTPHeaderFields?.merge(cookieHeader) { (_, new) in new }
        
        // 处理请求体(比如POST的JSON或表单数据)
        if let bodyData = request.body {
            urlRequest.httpBody = bodyData
        }
        
        // 发送转发请求(用信号量实现同步返回,适配GCDWebServer的同步回调)
        let semaphore = DispatchSemaphore(value: 0)
        var responseData: Data?
        var httpResponse: HTTPURLResponse?
        var requestError: Error?
        
        URLSession.shared.dataTask(with: urlRequest) { data, resp, err in
            responseData = data
            httpResponse = resp as? HTTPURLResponse
            requestError = err
            semaphore.signal()
        }.resume()
        
        semaphore.wait()
        
        if let error = requestError {
            print("Proxy request failed: \(error.localizedDescription)")
            return GCDWebServerResponse(statusCode: 500)
        }
        
        guard let resp = httpResponse, let data = responseData else {
            print("Empty response from backend")
            return GCDWebServerResponse(statusCode: 500)
        }
        
        // 构建返回给本地网页的响应
        let serverResponse = GCDWebServerDataResponse(data: data, contentType: resp.mimeType)
        serverResponse?.statusCode = resp.statusCode
        
        // 添加CORS头,允许本地网页跨域获取响应
        serverResponse?.setValue(self.localServerURL, forAdditionalHeader: "Access-Control-Allow-Origin")
        serverResponse?.setValue("true", forAdditionalHeader: "Access-Control-Allow-Credentials")
        
        // 同步后端返回的Set-Cookie到系统和WKWebView的Cookie存储
        if let setCookieHeaders = resp.allHeaderFields["Set-Cookie"] as? [String] {
            for cookieHeader in setCookieHeaders {
                guard let cookie = HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": cookieHeader], for: targetURL).first else { continue }
                // 同步到系统Cookie存储
                HTTPCookieStorage.shared.setCookie(cookie)
                // 同步到WKWebView的Cookie存储
                WKHTTPCookieStore.default.setCookie(cookie) {
                    // 可选:处理Cookie同步完成的回调
                }
            }
        }
        
        return serverResponse
    }
    
    private func startServer() {
        do {
            try webServer.start(withPort: 8080, bonjourName: "LocalProxyServer")
            print("Proxy server running at \(webServer.serverURL?.absoluteString ?? "")")
        } catch {
            print("Failed to start proxy server: \(error.localizedDescription)")
        }
    }
}

使用说明

  1. 初始化代理服务器:在App启动时创建LocalProxyServer实例,比如在AppDelegateapplication(_:didFinishLaunchingWithOptions:)方法中:
    let proxyServer = LocalProxyServer()
    
  2. 加载本地网页:用GCDWebServer的URL加载你的本地网页,比如:
    let webView = WKWebView()
    if let localPageURL = URL(string: "\(proxyServer.localServerURL)/index.html") {
        webView.load(URLRequest(url: localPageURL))
    }
    
  3. 修改网页请求地址:把网页中所有请求后端的URL替换成代理地址,比如原请求https://your-backend.com/api/login改成http://localhost:8080/proxy/api/login

注意事项

  • HTTPS后端处理:如果后端是自签名HTTPS证书,需要给URLSession配置信任(仅测试环境使用,生产环境请使用合法证书):
    let config = URLSessionConfiguration.default
    config.allowsInvalidCertificates = true
    let session = URLSession(configuration: config)
    
    然后用这个自定义session发送请求,替换代码中的URLSession.shared
  • CORS头配置:不要把Access-Control-Allow-Origin设为*,因为*Access-Control-Allow-Credentials: true不能同时生效,必须指定具体的本地服务器地址(比如http://localhost:8080)。
  • Cookie同步:确保WKWebView的Cookie和系统Cookie存储同步,这样代理转发请求时才能正确带上后端需要的Cookie。

内容的提问来源于stack exchange,提问作者Hudi Ilfeld

火山引擎 最新活动