Go后端OAuth2令牌存储、用户关联及受保护路由实现咨询
解决OAuth2受保护路由的核心问题
一、从令牌推断用户标识(无需额外调用userinfo)
你目前通过access_token调用Google的userinfo接口获取用户信息,其实Google OAuth2返回的token里包含ID Token(JWT格式),直接解析就能拿到邮箱、用户唯一ID等核心信息,无需额外发起请求。
步骤实现:
- 确保Google OAuth配置包含
openidscope(必须添加,否则无法获取ID Token):
func GoogleConfig() *oauth2.Config { return &oauth2.Config{ ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: os.Getenv("GOOGLE_REDIRECT_URL"), Scopes: []string{ "openid", // 必须添加该scope才能获取ID Token "email", "profile", }, Endpoint: google.Endpoint, } }
- 解析ID Token提取用户信息:
import "github.com/golang-jwt/jwt/v5" // 在Callback函数中,拿到token后执行以下逻辑 idToken, ok := token.Extra("id_token").(string) if !ok { return c.SendString("Failed to get ID Token") } // 解析并验证JWT签名(使用Google公钥) parsedToken, err := jwt.Parse(idToken, func(token *jwt.Token) (interface{}, error) { // 验证签名算法是否为RS256 if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } // 从Google获取公钥(生产环境建议缓存公钥,避免重复请求) certResp, err := http.Get("https://www.googleapis.com/oauth2/v3/certs") if err != nil { return nil, err } defer certResp.Body.Close() certBytes, _ := io.ReadAll(certResp.Body) var certMap map[string]string json.Unmarshal(certBytes, &certMap) // 根据token header中的kid匹配对应公钥 kid, ok := token.Header["kid"].(string) if !ok { return nil, fmt.Errorf("kid not found in token header") } cert := certMap[kid] return jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) }) if err != nil || !parsedToken.Valid { return c.SendString("Invalid ID Token") } // 提取用户核心信息 claims, _ := parsedToken.Claims.(jwt.MapClaims) email := claims["email"].(string) emailVerified := claims["email_verified"].(bool) googleUserID := claims["sub"].(string) // Google分配的用户唯一标识
二、令牌的存储方案
1. 给客户端的会话令牌(用于受保护路由验证)
不要直接将Google的access_token返回给客户端,应该生成自定义服务端会话令牌(比如JWT),通过HttpOnly Cookie或Authorization Header传递给客户端:
示例:生成自定义JWT并返回给客户端
import ( "time" "github.com/golang-jwt/jwt/v5" ) // 自定义JWT声明 type CustomClaims struct { UserID uint `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } // 在Callback函数的用户创建/查询逻辑之后 var targetUser models.User if existingUser.ID != 0 { targetUser = existingUser } else { targetUser = newUser // 注意:你原代码中创建newUser后未赋值给existingUser,需修正此逻辑 } // 生成会话JWT expirationTime := time.Now().Add(24 * time.Hour) claims := &CustomClaims{ UserID: targetUser.ID, Email: targetUser.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "your-app-name", // 替换为你的应用名称 }, } // 使用自定义密钥签名 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) if err != nil { return c.SendString("Failed to generate session token") } // 推荐使用HttpOnly Cookie存储令牌(防止XSS攻击) c.Cookie(&fiber.Cookie{ Name: "session_token", Value: tokenString, Expires: expirationTime, HTTPOnly: true, Secure: true, // 生产环境开启,仅HTTPS传输 SameSite: fiber.CookieSameSiteStrictMode, }) // 也可以返回令牌让客户端存在Authorization Header中 // return c.JSON(fiber.Map{"token": tokenString})
2. 存储Google的OAuth2令牌(用于后续API调用)
如果需要后续调用Google的服务API(比如用户日历、云端硬盘),需将Google返回的token(包含refresh_token)存入数据库,关联用户ID:
// 给User模型添加字段 type User struct { // ... 原有字段 GoogleAccessToken string `gorm:"type:text"` GoogleRefreshToken string `gorm:"type:text"` TokenExpiresAt time.Time `gorm:"type:datetime"` } // 在Callback函数中保存令牌 targetUser.GoogleAccessToken = token.AccessToken targetUser.GoogleRefreshToken = token.RefreshToken targetUser.TokenExpiresAt = token.Expiry database.DB.Db.Save(&targetUser)
三、实现受保护路由(中间件验证)
通过Fiber中间件验证会话令牌,验证通过后将用户信息注入上下文,供后续路由使用:
// 定义身份验证中间件 func AuthMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { // 从Cookie获取令牌,或从Authorization Header提取 tokenString := c.Cookies("session_token") if tokenString == "" { authHeader := c.Get("Authorization") if len(authHeader) > 7 && strings.HasPrefix(authHeader, "Bearer ") { tokenString = authHeader[7:] } else { return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized") } } // 解析并验证JWT claims := &CustomClaims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("JWT_SECRET")), nil }) if err != nil || !token.Valid { return c.Status(fiber.StatusUnauthorized).SendString("Invalid token") } // 将用户信息存入上下文 c.Locals("user_id", claims.UserID) c.Locals("user_email", claims.Email) // 继续执行后续路由逻辑 return c.Next() } } // 配置路由 func SetupRoutes(app *fiber.App) { app.Get("/callback", Callback) // 受保护路由组 protected := app.Group("/api") protected.Use(AuthMiddleware()) // 示例受保护路由:获取用户信息 protected.Get("/profile", func(c *fiber.Ctx) error { userID := c.Locals("user_id").(uint) var user models.User if err := database.DB.Db.First(&user, userID).Error; err != nil { return c.Status(fiber.StatusNotFound).SendString("User not found") } return c.JSON(user) }) }
注意事项
- 生产环境必须使用HTTPS,防止令牌被劫持
- JWT密钥需妥善保管,存入环境变量,禁止硬编码
- Google的refresh_token需加密存储,避免泄露
- 解析ID Token时,需验证签名、过期时间、受众(aud)等字段,确保令牌有效
内容的提问来源于stack exchange,提问作者Zev Ross
相关产品推荐
相关产品推荐

