直接用FusionAuth的OAuth2端点返回401,是因为其默认禁用CORS、Go服务未透传Authorization头、JWT签名密钥不一致、redirect URL未正确配置,以及验签时未处理kid字段或claims结构不匹配。

为什么直接用 FusionAuth 的 OAuth2 端点会 401?
因为 FusionAuth 默认不开放 /oauth2/authorize 和 /oauth2/token 的跨域(CORS)支持,且多数 Go 微服务用的是 net/http 或 gin,它们不会自动携带 Authorization: Bearer 到后端下游——你得自己透传。更关键的是,FusionAuth 的 JWT 签名密钥(application.jwtConfiguration.key)必须和 Go 服务验签时用的完全一致,否则 ParseWithClaims 直接 panic。
- FusionAuth 后台 → Applications → 你的应用 → JWT → 确保
Signing key类型是symmetric(如 HS256),并记下key值(base64 编码后的字符串) - Go 服务中不要硬编码该 key,应从环境变量读取:
FUSIONAUTH_JWT_KEY,并用base64.StdEncoding.DecodeString解码后再传给jwt.ParseWithClaims - 若 FusionAuth 部署在
https://auth.example.com,而你的微服务在https://api.example.com,则必须在 FusionAuth Admin UI 中为该 Application 设置正确的Authorized redirect URLs(含完整协议+域名+路径,如https://api.example.com/callback)
如何用 go-jose 验证 FusionAuth 发出的 JWT?
FusionAuth 默认签发 HS256 JWT,但 github.com/dgrijalva/jwt-go 已归档且对 kid 字段支持弱;推荐用 github.com/go-jose/go-jose/v3,它能正确处理 FusionAuth 的 kid(即使你用对称密钥,FusionAuth 仍会在 header 中写 "kid": "default")。
- 验证时必须显式指定
jwt.WithValidHeader("kid", "default"),否则Parse会因 header 不匹配失败 - 使用
jwt.ParseSigned而非jwt.Parse,因为 FusionAuth 的 token 是 signed,不是 encrypted - claims 结构体字段名必须全大写且带
json:tag,例如:UserId string `json:"userId"`—— FusionAuth 的标准 claim 是userId、email、roles(数组),不是sub或preferred_username
// 示例:解析并校验
token, err := jwt.ParseSigned(rawToken)
if err != nil { return err }
var claims struct {
UserId string `json:"userId"`
Email string `json:"email"`
Roles []string `json:"roles"`
}
if err := token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return err
}
// 手动校验 audience / issuer / exp
怎么让 Gin 中间件自动提取并验证 Authorization Header?
别依赖 c.GetHeader("Authorization") 然后手动切前缀——FusionAuth 返回的 token 是标准 Bearer 格式,但有些客户端(比如 Postman 自动注入)可能多空格或大小写错误(如 bearer)。中间件必须健壮处理。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
strings.CutPrefix替代strings.Split,避免切错导致空 token:token, ok := strings.CutPrefix(authHeader, "Bearer ") - 验证前先检查
len(token) > 0,空字符串会导致go-jose报square/go-jose: invalid JWT - 验签失败时返回
401 Unauthorized,但不要暴露具体失败原因(比如“签名无效”或“kid mismatch”),防止信息泄露 - 验证成功后,把
claims.UserId写入c.Set("user_id", claims.UserId),后续 handler 可通过c.GetString("user_id")获取
FusionAuth 的 Refresh Token 怎么安全续期?
Go 微服务本身不管理 refresh token 生命周期,它只负责转发请求。真正要小心的是:FusionAuth 的 /oauth2/token 接口要求 grant_type=refresh_token + refresh_token=xxx + client_id,且 client_secret 必须和申请授权码时一致。
- refresh token 必须通过 HTTPS 传输,且前端存储时禁止用 localStorage(易被 XSS 窃取),推荐用 httpOnly cookie(由 FusionAuth 的登录页 set)
- Go 服务收到 refresh 请求后,应构造
http.PostForm到 FusionAuth 的https://auth.example.com/oauth2/token,body 包含grant_type、refresh_token、client_id、client_secret - 响应中的新
access_token和refresh_token必须原样返回给前端,不要解码或修改——FusionAuth 的 refresh token 是一次性且带滚动(rotating)机制,旧 token 立即失效 - 如果 FusionAuth 返回
{"error":"invalid_grant"},大概率是 client_secret 不匹配,或 refresh token 已被用过一次
最常被忽略的一点:FusionAuth 的 Application 配置里,“Refresh Token Duration” 和 “Token Duration” 是两个独立设置,前者默认 7 天,后者默认 24 小时——如果你的 Go 服务 cache 了 token 但没同步刷新逻辑,用户会在 24 小时后突然 401,而不是平滑续期。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










