跨域预检报错?TaoToken Key 给 Codex 改 gin-contrib/cors 缓存

发布时间:2026/9/17 1:23:23
跨域预检报错?TaoToken Key 给 Codex 改 gin-contrib/cors 缓存
1. 登录页接口整合完成后跨域预检先来敲门跨域预检报错会卡住登录页根源是 Go 服务端没有响应浏览器先行的 OPTIONS 探针。我先去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 TaoToken 的 API Key再把它填给 Codex 当模型通道Base URL 填 https://taotoken.net/api。跨域策略的 AllowMethods、MaxAge 怎么写仍由 gin-contrib/cors 结合项目实际决定TaoToken 只负责统一接入模型请求。上一轮把后台登录页从占位逻辑切到真实请求后浏览器控制台开始出现 CORS 提示从 http://localhost:5173 发起的预检请求没能通过。登录接口的 POST 后端其实能收到但浏览器在发送 JSON 请求前会先发一个 OPTIONS 探针询问服务端“允不允许跨域”。这个探针没有拿到 Access-Control-Allow-Origin 响应头整个登录请求就被浏览器拦下页面上只留下一段看起来和业务无关的 CORS 报错。这时工作区里还堆着登录页整合那批改动。我习惯在改另一件事之前把工作区收干净于是先 git add 新增的 api 文件和 login.vue再整体 git stash等跨域配置改完再 git stash pop 接回来。注意新增文件如果不先 addstash 默认不会把它们一起带走。1.1 从“接口通了”到“Origin 被拒”隔了一个 preflight浏览器跨域规则对预检的条件分得很细。如果是 text/plain 或 form 表单提交可能不发预检一旦请求头带着 application/json 或自定义 header浏览器就会先发 OPTIONS 预检。登录接口提交的是 JSON所以第一个到达 Go 后端的其实是 OPTIONS /admin/login不是 POST /admin/login。Gin 默认路由没有单独注册 OPTIONS 方法跨域中间件也没挂上于是预检请求拿不到任何跨域响应头。浏览器的判断是“目标服务不允许跨域”后续真正的 POST 请求根本不会发出。这就是服务端日志里能看见 OPTIONS、前端却一直提示跨域的原因。想解决不是在前端加代理绕过而是让 Go 服务端直接响应预检并在响应头里给出允许的域名、方法和缓存时间。gin-contrib/cors 是 Go 项目里 star 数最高的跨域库直接把官方 GitHub 仓库地址发给 Codex它会照着最新版 API 来生成不会给你塞一份过时配置。选择这个库的另一个好处是它把预检缓存、域名白名单、请求头放行都抽象成配置项正好可以落进 yaml 文件统一管理。1.2 ~/.codex/config.toml 指到 TaoToken 通道Codex 默认会读 ~/.codex/config.toml。打开这个文件把模型供应商指向 TaoToken 的兼容通道。Key 从 TaoToken 控制台创建Base URL 填 https://taotoken.net/api末尾不要加 /v1。模型 ID 别靠记忆猜以模型广场当时列表为准。# ~/.codex/config.toml model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY然后在终端里把 Key 放进环境变量export TAOTOKEN_API_KEYYOUR_API_KEY配置完成后在项目目录启动 codex先让它读跨域报错和项目结构。Codex 只负责生成、解释和对照代码编译运行这些动作由你在本地完成它不会连接任何线上环境。现在可以把登录接口整合完成后的跨域报错原样贴给它同时让它检查项目里有没有已经存在的 cors.yaml。2. 给 Codex 的第一份材料报错原文 当前 cors.yaml2.1 把跨域报错和目录现状先贴给 Codex如果项目里还没有 config/cors.yaml 和 internal/middleware/cors.go把完整需求发给 Codex整合 gin-contrib/cors新建一个配置文件用于配置允许跨域的域名列表默认允许所有请求头和请求方法只在域名上做限制预检请求最大缓存时间 24 小时在 internal/middleware 下单独建立 cors.go最后到 api/main.go 里注册。一条可用的初始提示词是这样的现在解决跨域问题整合 github.com/gin-contrib/cors 跨域库。 新建 config/cors.yaml 配置允许跨域的域名列表默认允许所有请求头和请求方法只对域名做限制。 预检请求最大缓存时间设为 24 小时。 在 internal/middleware 下建立 cors.go使用以上跨域库结合配置文件实现 CORS并在 api/main.go 注册。Codex 第一版通常会把允许方法写成 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS 一长串。这不算错但后续想增加一种方法就要回头改中间件再发版。gin-contrib/cors 的 AllowMethods 支持 *让 Codex 直接改成通配更省事也更贴合“默认允许所有请求方法”的原始需求。2.2 从“手动列方法”到 *review 一次就够了AllowMethods 用 *意思是所有 HTTP 方法都放行。很多人会担心这样太危险但只要想清楚 CORS 的职责边界就不会慌CORS 解决的是浏览器跨域读取响应的问题服务端的登录接口仍然要靠 token 和权限中间件把关。跨域放行不等于匿名访问所以方法层面放开对开发效率的收益远大于风险。同样AllowHeaders 也建议用 *。前端登录请求可能携带 Content-Type、Authorization如果写死一份列表以后新增 header 就容易漏。真正需要严格控制的只有 AllowOrigins因为它是接口面的第一道门直接决定哪些前端页面能拿到你的响应。gin-contrib/cors 本身有一个默认行为要当心不设置 AllowOrigins 时可能会直接允许所有来源。对登录接口来说这绝对不能接受所以本文的 cors.yaml 里只放入 localhost 和 127.0.0.1开发环境够用线上部署时再按实际域名收敛。3. 预检缓存 24 小时默认值要写在 config/cors.yaml3.1 默认值放 yaml不要变成 Go 代码里的硬编码处理跨域配置最容易踩的坑是把默认值硬编码进 Go 代码。Codex 第一次生成时可能会写出“如果 methods 为空就默认 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONSheaders 为空就默认 *”的逻辑。表面看是给空配置兜底实际上会让 config/cors.yaml 失去意义——你清空某个字段后代码填回一套默认值配置文件里写的仍然是无效配置。正确做法是让 yaml 成为唯一事实来源。yaml 里写了什么就传给 gin-contrib/cors 什么yaml 里留空就保持 nil让库自己决定。不要让中间件额外包一层默认值逻辑。配置文件示例# config/cors.yaml allow_origins: - localhost - 127.0.0.1 allow_methods: - * allow_headers: - * max_age_hours: 24max_age_hours 是 24对应 86400 秒的预检缓存。同一个浏览器在 24 小时内再次发起同类型跨域请求不会再重复 OPTIONS 探针而是直接走正式请求。登录页反复进出的场景下这个缓存能让每次返回后台都少一次网络往返。3.2 域名白名单只填域名端口和协议交给中间件补如果前端地址是 http://localhost:5173、http://localhost:8080、https://localhost:8081你可能会在 yaml 里列三行带协议和端口的地址。但这种写法维护成本很高浏览器 Origin 里的端口一变配置就要跟着改。更合适的方式是白名单里只写裸域名在 cors.go 里解析请求的 Origin取出 Hostname 部分去白名单比对。import ( net/url time github.com/gin-contrib/cors github.com/gin-gonic/gin ) func CORS() gin.HandlerFunc { // cfg 由 config/cors.yaml 解析得到这里省略读取细节 allowed : make(map[string]struct{}, len(cfg.AllowOrigins)) for _, origin : range cfg.AllowOrigins { allowed[origin] struct{}{} } return cors.New(cors.Config{ AllowMethods: cfg.AllowMethods, AllowHeaders: cfg.AllowHeaders, MaxAge: time.Duration(cfg.MaxAgeHours) * time.Hour, AllowOriginFunc: func(origin string) bool { u, err : url.Parse(origin) if err ! nil { return false } _, ok : allowed[u.Hostname()] return ok }, }) }url.Parse 会把 Origin 拆成 scheme、host、portu.Hostname() 拿到的就是不带端口的 localhost。这样 localhost:5173 和 localhost:8080 都能命中同时 http 和 https 两种协议也都覆盖。第一次测试时就是吃了“域名还得加上端口号”的亏改成 Hostname 比对后一劳永逸。4. 本地启动实测curl 打一发 OPTIONS 预检4.1 观察 Access-Control-Max-Age 是否等于 86400中间件注册好之后在本地启动 Go 服务先用 curl 模拟一次跨域预检确认响应头没有缺项再回浏览器刷新登录页。命令如下curl -i -X OPTIONS http://127.0.0.1:8080/admin/login \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: content-type关键看响应头里有没有这几项Access-Control-Allow-Origin: http://localhost:5173 Access-Control-Allow-Methods: * Access-Control-Allow-Headers: * Access-Control-Max-Age: 86400Max-Age 单位是秒86400 正好等于 24 小时。如果它出现了说明预检缓存已经生效。如果 Allow-Origin 缺失检查白名单里的 hostname 是否和 Origin 解析出来的 Hostname 一致如果 Allow-Methods 没生效确认 yaml 里 allow_methods 不是空数组。如果 Codex 在排障过程中提示模型 ID 不可用先回 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场核对当前列表把 config.toml 里的 YOUR_MODEL_ID 替换成真实值。4.2 localhost:5173 和 127.0.0.1 是两个 Origin第一次测试时前端和后端都在本机下意识觉得 localhost 和 127.0.0.1 是一回事。实际浏览器把 Origin 字符串整体当作来源比较http://localhost:5173 和 http://127.0.0.1:5173 是截然不同的两个 Origin。如果你的前端地址栏是 127.0.0.1而 cors.yaml 里只写了 localhost预检照样失败。这个坑恰好验证了 3.2 用 Hostname 匹配的思路是对的配置里写 localhost比对时把端口和协议剥掉不管前端切到哪个端口都能过。把 curl 的响应头完整贴回给 Codex它会自己判断是哪一项没有对齐。本地测试通过后再 git stash pop 把登录页整合的改动恢复出来连起来跑一遍真实登录流程。5. 复盘与后续Codex 改配置我给结论5.1 这轮修复里Codex 承担了什么Codex 在这次排障里做了三件事分析 OPTIONS 预检失败的原因生成 gin-contrib/cors 的整合代码根据 review 意见把 AllowMethods 改成 *、把默认值搬进 yaml、用 Hostname 匹配域名白名单。它没有直接连接任何生产环境所有改动都落在本地工作区最后由我在本地启动验证再把结果贴回对话继续修正。跨域配置看起来只有几十行却直接影响接口面是否暴露给不该访问的页面。白名单写成 * 的风险很高所以 Codex 的初版生成完人工 review 反而比生成更关键。我的检查原则是配置文件是唯一事实来源代码里不藏着隐式默认值白名单尽量窄开发环境只放 localhost 和 127.0.0.1方法放开没问题接口鉴权必须留在服务端。登录请求绑定从 form 改成 json、后端字段 tag 从 PascalCase 对到 snake_case这些其实是在跨域修完、整个登录流程串起来之后才暴露出来的属于同一个测试周期的产物。5.2 回到控制台对一下这次调用排障过程中 Codex 每轮修改都会消耗模型调用跑通之后可以到 模型对话 用同一把 Key 发一条测试消息确认调用计费正常如果接下来要长期让 Codex 或 Claude Code 写项目代码打开 Coding Plan 看套餐是否够用。Key 统一在 控制台 API Keys 创建Claude Code 的环境变量写法见 接入文档。