redirecturl必须逐字节匹配(协议/域名/端口/路径/末尾斜杠),否则静默400或返回invalid_request;state须用crypto/rand安全生成、绑定session、恒定时间校验;conf.exchange()必须传r.context(),三者缺一即失败。

RedirectURL 必须逐字节匹配,否则静默 400
OAuth2 平台(GitHub/Google/微信/Keycloak)对 RedirectURL 的校验是严格字节级的:协议、域名、端口、路径、末尾斜杠,缺一不可。填错不会返回明确提示,大概率直接静默 400 或返回 {"error":"invalid_request"}。
- 生产环境必须用
https://example.com/auth/callback,且证书有效;本地开发可用http://localhost:8080/auth/callback,但不能混用 - 反向代理(如 Nginx)需透传
X-Forwarded-Proto: https,否则 Gin 拿到的r.URL.Scheme是http,拼出的回调地址就错了 - 微信要求
redirect_uri在AuthCodeURL()中做 URL 编码(如%2Fcallback);GitHub/Google 明确拒绝编码过的 URI——别复用同一套拼接逻辑 - 多环境部署时,从环境变量注入:
os.Getenv("OAUTH_REDIRECT_URL"),禁止硬编码或 if 分支判断
state 参数不生成、不绑定 session、不恒定时间比对 = 开门揖盗
golang.org/x/oauth2 包完全不处理 state,漏掉它等于把授权入口敞开:攻击者伪造跳转链接,用户一点,code 就被发到他服务器上换 access_token。
- 生成:用
crypto/rand.Read取 32 字节,再hex.EncodeToString;禁用math/rand - 存储:写进 session,key 为
"oauth_state:" + sessionID(Redis 或内存 session 都要绑定会话 ID),不能全局共享,也不能明文放 query 或 localStorage - 比对:回调 handler 中先取
r.URL.Query().Get("state"),再查 session 是否存在且未过期,用subtle.ConstantTimeCompare恒定时间比对,成功后立刻session.Delete("oauth_state") - 别往
state里塞next=/admin或user_id=123——它只防 CSRF;业务跳转走后端 session 或 JWT payload
conf.Exchange() 失败,90% 是 context、body、error 三处没对齐
conf.Exchange(r.Context(), code) 报错常见现象是 invalid_grant、bad_verification_code 或空 token,但根源往往不在 OAuth 服务端,而在 Gin 客户端调用姿势。
- 必须传
r.Context(),不是context.Background();否则超时、取消控制全失效 - 确认中间件(如
gin.Recovery()、日志、JWT 解析)没提前调用r.Body.Read()——否则Exchange()读到空 body,报invalid_request - 错误类型必须细判:
*oauth2.RetrieveError会带 HTTP 状态码和原始错误体(如{"error":"invalid_grant"});net/http超时错误要单独捕获;JSON 解析失败需 fallback 提示
别信封装好的 gin-oauth2 中间件,它不处理 state 和 context 传递
像 github.com/appleboy/gin-oauth2 这类第三方中间件,表面封装了流程,但关键安全环节(state 生成/校验、context 传递、RedirectURL 动态构造)基本交由使用者自行补全,实际用起来反而容易漏。
- 它默认用
context.Background()调Exchange(),超时控制形同虚设 - 不提供
state绑定 session 的逻辑,也不做恒定时间比对 - RedirectURL 写死在配置里,无法按请求动态适配(比如多租户子域名场景)
- 建议手写核心 handler:控制
AuthCodeURL()构造、state生命周期、Exchange()调用及错误分支,其余可复用golang.org/x/oauth2原生能力











