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

Gin框架API隔次请求无法接收Authorization Header问题排查

iOS客户端二次请求丢失Authorization Header问题排查与解决

问题描述

用Go Gin重构API后,iOS客户端发起请求出现异常:首次请求携带Authorization Header可正常通过鉴权,但第二次切换不同端点请求时,该Header丢失导致被拦截。此前用TypeScript实现时无此问题,仅Gin重构后出现。

复现步骤

  • 从iOS客户端调用/estimate端点,请求成功
  • 再次调用另一个POST端点(如/),请求因缺少Authorization Header失败

服务端核心代码

package main

import (
    "github.com/gin-gonic/gin"
)

// 省略其他导入与无关代码...

func Routes(r *gin.Engine, db *mongo.Database, g *gateman.Gateman, conn *amqp091.Connection) {
    atHandler := pc.NewPackagesController(ps.NewPackagesService(pr.NewPackagesRepository(db)), g, events.NewEventEmitter(conn))

    r.Use(CORS())
    v1 := r.Group("/api/v1/package")
    {
        v1.POST("/query", GraphqlHandler(db, directives.NewDirectivesManager(g)))
        v1.GET("/", PlaygroundHandler(db))
        v1.POST("/", g.Guard([]string{"user"}, nil), atHandler.Create)
        v1.POST("/estimate", g.Guard([]string{"user"}, nil), atHandler.Estimate)
        // 省略其他路由定义
        v1.GET("/healthz", atHandler.GetHealth)
    }
    r.GET("/", atHandler.GetUP)
}

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

// 省略其他代码...

客户端核心代码

func request<T>(with builder: BaseRequest, customDecoder: JSONDecoder) -> AnyPublisher<T, APIError> where T: Codable {
    let encoding: ParametersEncoder = [.get, .delete].contains(builder.method) ? URLParameretersEncoder() : JSONParametersEncoder()
    customDecoder.keyDecodingStrategy = .convertFromSnakeCase
    
    var url: URL {
        var components = URLComponents()
        components.scheme = "https"
        components.host = builder.baseUrl
        components.path = "/api/v1" + builder.path
        
        guard let url = components.url else {
            preconditionFailure("Invalid URL components: \(components)")
        }
        return url
    }
    
    var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 46.0)
    urlRequest.httpMethod = builder.method.rawValue
    
    builder.headers.forEach { key, value in
        urlRequest.setValue(value, forHTTPHeaderField: key)
    }
    
    if let token = tokenManager.token {
        urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
    }
    
    if let parameters = builder.parameters {
        guard let encoded = try? encoding.encode(parameters: parameters, in: urlRequest) else {
            fatalError()
        }
        urlRequest = encoded
    }
    
    self.log(request: urlRequest)
    
    return URLSession.shared
        .dataTaskPublisher(for: urlRequest)
        .receive(on: DispatchQueue.main)
        .mapError {_ -> APIError in
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            return .unknown
        }
        .flatMap { data, response -> AnyPublisher<T, APIError> in
            // 省略响应处理代码
        }
        .eraseToAnyPublisher()
}

protocol BaseRequest {
    var baseUrl: String { get }
    var path: String { get }
    var headers: HTTPHeaders { get }
    var method: HTTPRequestMethod { get }
    var parameters: HTTPParameters { get }
}

public typealias HTTPHeaders = [String: String]
public typealias HTTPParameters = [String: Any]?

预期与实际结果

  • 预期:所有受鉴权保护的端点,只要请求携带有效Authorization Header就应通过
  • 实际:首次请求任意POST端点成功;切换不同POST端点时,第二次请求丢失Authorization Header导致失败。Postman测试无此问题,仅iOS客户端出现,多次调用同一端点无异常。

环境信息

  • Go版本:1.19
  • Gin版本:v1.8.1
  • 操作系统:Mac开发机、iOS移动端

问题根源与解决方案

1. CORS配置冲突(核心问题)

当前CORS设置中,Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true违反HTTP规范:允许携带凭证时,Access-Control-Allow-Origin不能设为通配符*,必须指定具体源。iOS的URLSession会因该不合法配置,在后续跨域请求中自动移除Authorization Header。

修复代码:
修改CORS中间件,动态获取请求Origin并设置(可添加白名单校验确保安全):

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 动态获取请求Origin
        origin := c.Request.Header.Get("Origin")
        // 可选:添加白名单校验,仅允许信任的源
        // allowedOrigins := []string{"https://your-app-domain.com"}
        // for _, allowed := range allowedOrigins {
        //     if allowed == origin {
        //         c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
        //         break
        //     }
        // }
        c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

2. 检查客户端编码逻辑

确认JSONParametersEncoder实现是否会覆盖原有Header。若编码函数内部重新创建URLRequest但未复制原Header,会导致Authorization丢失。

验证建议:
在编码后添加日志,确认Header是否存在:

if let parameters = builder.parameters {
    guard let encoded = try? encoding.encode(parameters: parameters, in: urlRequest) else {
        fatalError()
    }
    urlRequest = encoded
    // 打印编码后的Header
    print("Encoded request headers: \(urlRequest.allHTTPHeaderFields ?? [:])")
}

若Header丢失,修改编码器逻辑,确保复制原请求所有Header后再添加参数。

3. 确认中间件顺序

当前代码中r.Use(CORS())在路由组定义前,确保了CORS中间件优先执行,无需调整。若后续新增中间件,需保证CORS始终是第一个被应用的中间件,避免OPTIONS请求被提前拦截。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 03:25:19