我正在使用 Gin 框架在 Go 中开发一个 Web 应用程序,需要 Google OAuth2 身份验证和 Google Drive 集成。我当前的实现可以工作,但存在刷新令牌、令牌过期和频繁的重新身份验证提示等问题。
这是我的设置摘要:
OAuth2 设置:我使用 markbates/goth 和 gothic 来处理 Google OAuth2 身份验证。
令牌管理:我将访问令牌和刷新令牌保存到 token.json 文件中。
令牌刷新逻辑:我检查令牌是否过期并尝试使用刷新令牌刷新它。
问题: 未提供刷新令牌:有时,即使在提示用户同意后我也没有收到刷新令牌。
令牌过期处理:当访问令牌过期时,我的应用程序有时无法刷新令牌并需要用户重新进行身份验证。
频繁重新验证提示:每次验证时,应用程序都会提示我选择帐户并再次授予权限。
背景: 我确保设置 access_type=offline 并提示您最初同意获取刷新令牌。
我安全地保存访问令牌和刷新令牌。
在检查令牌是否过期时,我尝试使用刷新令牌刷新访问令牌。
问题: 如何确保我始终收到来自 Google 的刷新令牌?
在我当前的设置中处理令牌过期和刷新的最佳方法是什么?
是否有更安全的方式来管理令牌,尤其是刷新令牌,以避免重新验证提示?
如何避免每次身份验证时都提示选择帐户并授予权限?
任何指导或建议将不胜感激。谢谢!
func InitGoogleAuth() {
err := godotenv.Load()
if err != nil) {
log.Fatalf("Error loading .env file: %v", err)
}
googleClientID := os.Getenv("GOOGLE_CLIENT_ID")
googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
googleScopes := []string{
"openid",
"profile",
"email",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive",
}
store := sessions.NewCookieStore([]byte(key))
store.Options = &sessions.Options{
Path: "/",
MaxAge: MaxAge,
HttpOnly: true,
Secure: IsProd,
}
gothic.Store = store
log.Println("Initializing Google provider with Client ID:", googleClientID)
log.Println("Using Scopes:", googleScopes)
// Initialize Google provider with updated scopes and access type
googleProvider := google.New(googleClientID, googleClientSecret, "http://localhost:8080/v1/auth/google/callback", googleScopes...)
googleProvider.SetAccessType("offline")
googleProvider.SetPrompt("consent")
goth.UseProviders(googleProvider)
log.Println("Google provider initialized with scopes and offline access.")
}
func AuthCallback(c *gin.Context) {
provider := c.Param("provider")
type contextKey string
const providerKey contextKey = "provider"
ctx := context.WithValue(c.Request.Context(), providerKey, provider)
c.Request = c.Request.WithContext(ctx)
session, err := gothic.Store.Get(c.Request, "gothic-session")
if err != nil {
log.Println("Error retrieving session in AuthCallback:", err)
} else {
log.Println("Session retrieved in AuthCallback, session ID:", session.ID)
}
user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprint(err))
return
}
token := &oauth2.Token{
AccessToken: user.AccessToken,
RefreshToken: user.RefreshToken,
Expiry: user.ExpiresAt,
}
saveToken("token.json", token)
var existingUser models.User
err = db.DB.QueryRow("SELECT id, username, password, email FROM users WHERE email = ?", user.Email).Scan(&existingUser.ID, &existingUser.Username, &existingUser.Password, &existingUser.Email)
if err == nil {
log.Println("User already exists:", existingUser.Email)
// Generate tokens for existing user
accessToken, err := utils.GenerateAccessToken(existingUser.Username)
if err != nil {
log.Println("Error generating access token:", err)
c.String(http.StatusInternalServerError, fmt.Sprintf("Error generating access token: %v", err))
return
}
refreshToken, err := utils.GenerateRefreshToken(existingUser.Username)
if err != nil {
log.Println("Error generating refresh token:", err)
c.String(http.StatusInternalServerError, fmt.Sprintf("Error generating refresh token: %v", err))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("http://localhost:5173/auth/google/callback?email=%s&username=%s&access_token=%s&refresh_token=%s&id=%d", existingUser.Email, existingUser.Username, accessToken, refreshToken, existingUser.ID))
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("http://localhost:5173/choose-username?email=%s", user.Email))
}
func refreshAccessToken(tok *oauth2.Token) (*oauth2.Token, error) {
config := &oauth2.Config{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
Scopes: []string{drive.DriveScope},
Endpoint: google.Endpoint,
}
// Use the refresh token to get a new access token
newToken, err := config.TokenSource(context.Background(), tok).Token()
if err != nil {
return nil, fmt.Errorf("unable to refresh token: %v", err)
}
// Save the new token to the file
saveToken("token.json", newToken)
return newToken, nil
}
我尝试过的: 初始设置:使用 access_type=offline 和 SetPrompt("consent") 配置 Google OAuth2。
令牌管理:在 token.json 文件中保存访问令牌和刷新令牌。
令牌刷新逻辑:检查访问令牌是否过期并尝试使用刷新令牌刷新它。
处理过期令牌:尝试在服务器启动期间和进行 API 调用之前刷新令牌。
我的期望: 在用户同意的情况下一致地接收刷新令牌。
使用刷新令牌获取新的访问令牌,无需额外的用户提示。
无缝处理令牌过期,避免频繁的重新身份验证请求。
实际发生的事情: 未提供刷新令牌:有时,即使在提示用户同意后我也没有收到刷新令牌。
令牌过期处理:当访问令牌过期时,我的应用程序有时无法刷新令牌,需要用户重新进行身份验证。
频繁的重新验证提示:每次验证时,应用程序都会提示我选择帐户并再次授予权限,这不是我想要的用户体验。
每当用户授予您同意时,您都会获得刷新令牌,并且您可以将授权代码交换为访问令牌和刷新令牌(如果您请求离线访问)。
googleProvider.SetAccessType("offline")
如果您刷新刷新令牌以获得新的访问令牌,您将不会总是获得新的刷新令牌。 如果你创建了一个本机桌面应用程序,我认为你总是能得到它,但对于网络应用程序来说,你有时会得到它,但不是每次都能得到它。
确保始终存储最新的刷新令牌。
token.json 请记住,每个用户都需要一个。您确实应该将其与用户帐户一起存储在数据库中。 存储您上次获得的时间,然后您可以使用它来决定是否需要新的。