gin 本身不内置 sso 能力,但可通过 jwt 鉴权中间件验证由独立认证中心(idp)签发的 token,实现低成本 sso 接入;关键在于校验签名、过期、iss/aud 声明,并将用户信息注入 gin.context 供后续处理。

直接说结论:Gin 本身不内置 SSO 能力,但用 jwt + 中间件 + 统一认证服务(如独立的 SSO Server),就能在 Gin 应用里低成本接入 SSO,关键不是“Gin 怎么做 SSO”,而是“Gin 怎么验证别人发来的 SSO Token”。
怎么验证从 SSO 中心发来的 JWT Token
绝大多数 Gin SSO 场景,你的 Gin 服务只是 Service Provider(SP),Token 由 IdP(比如 Keycloak、自建 OAuth2 服务或 CAS)签发。你只需校验它是否合法、未过期、签名正确、且没被吊销。
- 别自己解析
token字符串——用github.com/golang-jwt/jwt/v5的jwt.Parse,传入正确的SigningMethod和keyFunc -
keyFunc不能硬编码密钥;如果是对称密钥(HS256),所有 SP 必须和 IdP 共享同一secret;如果是非对称(RS256),要用jwt.PublicKeyProvider加载公钥 - 必须检查
claims["iss"](Issuer)是否匹配你的 IdP 地址,防止 Token 被其他系统伪造复用 - 建议额外校验
claims["aud"](Audience),确保该 Token 是发给当前 Gin 服务的,比如设为"api.example.com"
怎么把用户信息注入 Gin Context
验证通过后,别急着写业务逻辑——先把用户身份塞进 c(*gin.Context),后续 Handler 才能统一取用。
- 用
c.Set("user_id", claims["sub"])或c.Set("username", claims["preferred_username"]),字段名按 IdP 返回的 JWT payload 定义走 - 避免在中间件里调用
c.Abort()后还继续执行后续逻辑;推荐统一返回c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"}) - 如果需要 session 级别的上下文(比如权限列表),可在验证 Token 后,用
user_id查 Redis 缓存(不要每次查 DB),再c.Set("permissions", perms)
为什么不能只靠 Cookie 实现跨域 SSO
浏览器同源策略下,.a.com 和 .b.com 无法共享 Cookie,所以单纯靠 Gin 的 c.SetCookie 做不到真正跨域 SSO。
- 前端必须显式携带 Token:登录后把
id_token存 localStorage,所有请求加Authorization: Bearer <token></token>头 - 如果非要 Cookie 方案,得让所有子域共用根域名(如
app.example.com/api.example.com),然后SetCookie的Domain设为.example.com - CAS 协议里常见的
ticket是一次性的,Gin 接收后要立刻拿去 IdP 的/validate接口换用户信息,不能缓存 ticket 本地
Redis 黑名单怎么配合 JWT 做登出控制
JWT 默认是无状态的,没法主动失效。想支持“单点退出”,就得引入外部状态存储。
- 用户登出时,把 Token 的
jti(JWT ID)和过期时间写入 Redis,设置 TTL 略长于 Token 过期时间(比如 Token 2 小时,Redis 存 2 小时 5 分钟) - 鉴权中间件里,先
jwt.Parse,再用redis.Get(ctx, "jti:" + jti)查黑名单;命中就拒绝 - 注意:Redis key 建议加前缀(如
sso:blacklist:jti:),避免和其他业务 key 冲突;同时用 pipeline 减少 RTT
最常被忽略的一点:IdP 返回的 Token 如果是 access_token(OAuth2),它通常不包含用户身份字段(如 sub、email),你需要额外调用 IdP 的 /userinfo 接口获取——这时候 Gin 中间件就得发起 HTTP 请求,要考虑超时和错误重试,不能简单当作 JWT 解析。











