package auth import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" ) // Google OAuth2(OIDC 简化用法:openid email profile 三个 scope, // userinfo 接口拿资料)。配置来源 .env 的 ONE_GOOGLE_CLIENT_ID / SECRET。 const ( googleAuthURL = "https://accounts.google.com/o/oauth2/v2/auth" googleTokenURL = "https://oauth2.googleapis.com/token" googleUserURL = "https://openidconnect.googleapis.com/v1/userinfo" ) type Google struct { ClientID string ClientSecret string } func (g Google) Enabled() bool { return g.ClientID != "" && g.ClientSecret != "" } func (g Google) LoginURL(redirectURI, state string) string { v := url.Values{} v.Set("client_id", g.ClientID) v.Set("redirect_uri", redirectURI) v.Set("response_type", "code") v.Set("scope", "openid email profile") v.Set("state", state) return googleAuthURL + "?" + v.Encode() } // GoogleUser 是 userinfo 接口里我们关心的字段。 // sub 是 Google 账号的稳定唯一 id;邮箱需要已验证才当 handle 用。 type GoogleUser struct { Sub string `json:"sub"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Name string `json:"name"` Picture string `json:"picture"` } func (g Google) Exchange(ctx context.Context, code, redirectURI string) (string, error) { v := url.Values{} v.Set("client_id", g.ClientID) v.Set("client_secret", g.ClientSecret) v.Set("code", code) v.Set("redirect_uri", redirectURI) v.Set("grant_type", "authorization_code") req, err := http.NewRequestWithContext(ctx, http.MethodPost, googleTokenURL, strings.NewReader(v.Encode())) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer res.Body.Close() var out struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(res.Body).Decode(&out); err != nil || out.AccessToken == "" { return "", fmt.Errorf("google: token exchange failed") } return out.AccessToken, nil } func (g Google) FetchUser(ctx context.Context, accessToken string) (GoogleUser, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, googleUserURL, nil) if err != nil { return GoogleUser{}, err } req.Header.Set("Authorization", "Bearer "+accessToken) res, err := http.DefaultClient.Do(req) if err != nil { return GoogleUser{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return GoogleUser{}, fmt.Errorf("google: userinfo %d", res.StatusCode) } var u GoogleUser if err := json.NewDecoder(res.Body).Decode(&u); err != nil { return GoogleUser{}, err } if u.Sub == "" { return GoogleUser{}, fmt.Errorf("google: userinfo missing sub") } return u, nil }