可行,但需明确 Gin 仅作为 SP 对接外部 IdP(如 Keycloak)或内网自建轻量 JWT+Redis IdP,严禁手写 OIDC 流程;推荐 go-oidc 库,issuer 必须完整 URL,aud 须与 client_id 完全一致,JWT 签名密钥须强随机,payload 仅含必要字段,校验必须严格。

自建 Gin 单点 SSO 系统可行,但必须明确:它不是“独立实现 OIDC/SAML”,而是作为 Service Provider(SP)对接外部 Identity Provider(IdP),或在可信内网中用 JWT + Redis 自建轻量 IdP。直接手写 OIDC 流程、签名 ID Token、处理 nonce/state、校验 JWKS 是高危操作,别干。
用 Gin 实现 SP 端对接 Google / Keycloak 等 OIDC 提供商
这是最常见、最安全的落地方式。Gin 只负责接收回调、验证 ID Token、建立本地会话,不碰登录页和令牌签发。
-
go-oidc是唯一推荐库,它由 Go 团队维护,严格遵循 RFC 7519 和 OIDC Core;别用封装过重或已归档的第三方 oidc 包 - 初始化
verifier时,issuer必须是完整 URL(如https://accounts.google.com),末尾斜杠不能省——https://google.com或accounts.google.com都会导致iss校验失败 -
aud字段必须与你在 IdP 后台注册的client_id完全一致;大小写、拼写、是否带前缀(如apps-)都要对得上 - 回调路由里用
c.Request().URL.Query().Get("code")直接取参数,别依赖c.Query("code")——某些 Gin 版本或中间件(如 Nginx proxy_pass)会静默丢弃非法 UTF-8 字符,导致 code 为空 - 拿到
id_token后,只用它提取用户身份(email、sub),绝不把它当access_token去调用用户 API;要用access_token单独请求/userinfo或其他资源端点
用 Gin + JWT + Redis 自建内网轻量 IdP(非标准 OIDC)
适合企业内部多个 Gin 应用共享登录态,不要求兼容外部系统。核心是「服务端签发短时效 JWT」+「Redis 存 refresh token 或黑名单」,而非把所有状态扔给前端。
- JWT 签名密钥必须用
crypto/rand.Read()生成 ≥32 字节随机字节,别用time.Now().String()或环境变量字符串——前者可预测,后者易泄漏 - payload 中只放必要字段:
user_id、role、exp、iss(固定为"sso.internal");别塞email、phone、密码哈希等敏感信息 - JWT 过期时间建议 ≤15 分钟;配合 Redis 存
refresh_token(UUID)+ 对应的user_id和新exp,每次刷新都重置 Redis TTL - 校验 JWT 时,必须调用
token.Claims.VerifyExpiresAt(time.Now(), true)+token.Header["alg"] == "HS256"+claims["iss"] == "sso.internal";只解析不校验等于裸奔 - 前端传来的 JWT 必须从
Authorization: Bearer <token>头里取,禁止从 query 或 cookie 读——否则无法防 CSRF,且 cookie 易被HttpOnly拦截
redirect_uri 不一致导致 invalid_request: Missing required parameter: code
这是 OAuth 2.0 授权码流程中最常卡住的地方,90% 问题出在 URL 字符串完整性上。
- 你在 IdP 后台配置的
redirect_uri(如https://app.example.com/callback)必须与代码中传给provider.AuthURL的**完全一致**:协议(https)、host(app.example.com)、port(如有)、path(/callback)、末尾斜杠(有/无)全部匹配 - 如果用了反向代理(如 Nginx),检查是否启用了
proxy_redirect或proxy_pass_request_headers off;Nginx 默认会对 query string 做一次 decode,可能把code=AbC123...变成空值 - Gin 路由定义必须是精确匹配:
router.GET("/callback", callbackHandler),不能写成router.GET("/callback/*any", ...)——通配符会破坏 query 参数透传 - 若前端是 SPA(如 Vue Router history 模式),确保
/callback是真实由 Gin 处理的 endpoint,而不是被前端 router 拦截后返回 404
JWT 存 Cookie 还是 Authorization Header?
存 Header 是更干净的选择,尤其当你有多个子域应用(如 app1.example.com、app2.example.com)时,Cookie 的 domain 限制会成为障碍。
- 用
http.SetCookie存 JWT 本身风险很高:一旦 XSS 成功,JS 可直接读取 cookie 内容(除非设HttpOnly,但那样前端 JS 就拿不到 token 刷新了) - 若坚持用 Cookie,必须同时设置:
HttpOnly=true、Secure=true(仅 HTTPS)、SameSite=Strict或Lax;且 cookie name 别叫token或jwt,避免被自动化扫描工具识别 - 更推荐方案:登录成功后,后端返回 JSON { "access_token": "xxx", "expires_in": 900 },前端存在
localStorage或内存,并在后续请求 header 中带上Authorization: Bearer xxx;JWT 校验逻辑统一放在 Gin middleware 里 - 注意:
Authorizationheader 方案要求所有跨域请求都显式允许该 header,API 响应头需包含Access-Control-Allow-Headers: Authorization
真正难的不是签发 token,而是让每个环节都拒绝“差不多就行”:issuer URL 多一个斜杠、state 参数没存 session、refresh token 没设 Redis 过期、JWT exp 检查被注释掉……这些地方不出问题则已,一出就是越权或会话劫持。


















