使用 Go 语言实现 Google 的 Oauth2

2025-06-04

使用 Go 语言实现 Google 的 Oauth2

身份验证是任何应用程序中最常见的部分。您可以实现自己的身份验证系统,也可以使用现有的众多替代方案之一,但在本例中,我们将使用 OAuth2。

OAuth 是一种规范,允许用户委托对其数据的访问权限,而无需与该服务共享其用户名和密码,如果您想了解有关 Oauth2 的更多信息,请点击此处

配置 Google 项目

首先,我们需要创建我们的 Google 项目并创建 OAuth2 凭据。

  • 前往 Google Cloud Platform
  • 创建一个新项目或选择一个(如果已有)。
  • 转到“凭据”,然后选择“OAuth 客户端 ID”创建一个新的凭据
  • 对于此示例,添加“授权重定向 URL”localhost:8000/auth/google/callback
  • 复制client_id和客户端密钥

OAuth2 如何与 Google 配合使用

当您的应用程序将浏览器重定向到 Google 网址时,授权序列就开始了;该网址包含指示所请求访问类型的查询参数。Google 负责处理用户身份验证、会话选择和用户同意。授权结果是授权码,应用程序可以用它来交换访问令牌和刷新令牌。

应用程序应存储刷新令牌以备将来使用,并使用访问令牌访问 Google API。访问令牌过期后,应用程序将使用刷新令牌获取新的访问令牌。

Oauth2Google

让我们来看看代码

我们将使用“golang.org/x/oauth2”包,该包为进行 OAuth2 授权和身份验证的 HTTP 请求提供支持。

在您的工作目录中创建一个新项目(文件夹),在我的情况下,我将其称为“oauth2-example”,并且我们需要包含oauth2的包。

go get golang.org/x/oauth2

因此我们在项目中创建一个main.go。

package main

import (
    "fmt"
    "net/http"
    "log"
    "github.com/douglasmakey/oauth2-example/handlers"
)

func main() {
    server := &http.Server{
        Addr: fmt.Sprintf(":8000"),
        Handler: handlers.New(),
    }

    log.Printf("Starting HTTP Server. Listening at %q", server.Addr)
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Printf("%v", err)
    } else {
        log.Println("Server closed!")
    }
}

Enter fullscreen mode Exit fullscreen mode

我们使用 http.Server 创建一个简单的服务器并运行。

接下来,我们创建包含应用程序处理程序的文件夹“handlers”,在此文件夹中创建“base.go”。

package handlers

import (
    "net/http"
)

func New() http.Handler {
    mux := http.NewServeMux()
    // Root
    mux.Handle("/",  http.FileServer(http.Dir("templates/")))

    // OauthGoogle
    mux.HandleFunc("/auth/google/login", oauthGoogleLogin)
    mux.HandleFunc("/auth/google/callback", oauthGoogleCallback)

    return mux
}
Enter fullscreen mode Exit fullscreen mode

我们使用http.ServeMux来处理我们的端点,接下来我们创建根端点“/”来提供具有最少 HTML 和 CSS 的简单模板,在这个例子中我们使用“http.http.FileServer”,该模板是“index.html”并且位于文件夹“templates”中。

另外,我们为 Google 的 Oauth 创建了两个端点,分别为“/auth/google/login”和“/auth/google/callback”。还记得我们在 Google 控制台中配置应用程序时的操作吗?回调 URL 必须相同。

接下来,我们在处理程序中创建另一个文件,我们将其命名为“oauth_google.go”,该文件包含在我们的应用程序中处理与 Google 的 OAuth 的所有逻辑。

我们通过 auth.Config 声明 var googleOauthConfig 来与 Google 进行通信。
范围:OAuth 2.0 范围提供了一种限制授予访问令牌的访问量的方法。

var googleOauthConfig = &oauth2.Config{
    RedirectURL:  "http://localhost:8000/auth/google/callback",
    ClientID:     os.Getenv("GOOGLE_OAUTH_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"),
    Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email"},
    Endpoint:     google.Endpoint,
}
Enter fullscreen mode Exit fullscreen mode

处理程序 oauthGoogleLogin

该处理程序创建一个登录链接并将用户重定向到该链接:

AuthCodeURL 接收 state 令牌,用于保护用户免受 CSRF 攻击。您必须始终提供一个非空字符串,并验证其是否与重定向回调中的 state 查询参数匹配。建议每次请求都随机生成此令牌,因此我们使用了一个简单的 cookie。

func oauthGoogleLogin(w http.ResponseWriter, r *http.Request) {

    // Create oauthState cookie
    oauthState := generateStateOauthCookie(w)
    u := googleOauthConfig.AuthCodeURL(oauthState)
    http.Redirect(w, r, u, http.StatusTemporaryRedirect)
}

func generateStateOauthCookie(w http.ResponseWriter) string {
    var expiration = time.Now().Add(365 * 24 * time.Hour)

    b := make([]byte, 16)
    rand.Read(b)
    state := base64.URLEncoding.EncodeToString(b)
    cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration}
    http.SetCookie(w, &cookie)

    return state
}

Enter fullscreen mode Exit fullscreen mode

处理程序 oauthGoogleCallback

该处理程序检查状态是否等于 oauthStateCookie,并将代码传递给函数getUserDataFromGoogle

func oauthGoogleCallback(w http.ResponseWriter, r *http.Request) {
    // Read oauthState from Cookie
    oauthState, _ := r.Cookie("oauthstate")

    if r.FormValue("state") != oauthState.Value {
        log.Println("invalid oauth google state")
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    data, err := getUserDataFromGoogle(r.FormValue("code"))
    if err != nil {
        log.Println(err.Error())
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    // GetOrCreate User in your db.
    // Redirect or response with a token.
    // More code .....
    fmt.Fprintf(w, "UserInfo: %s\n", data)
}

func getUserDataFromGoogle(code string) ([]byte, error) {
    // Use code to get token and get user info from Google.

    token, err := googleOauthConfig.Exchange(context.Background(), code)
    if err != nil {
        return nil, fmt.Errorf("code exchange wrong: %s", err.Error())
    }
    response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
    if err != nil {
        return nil, fmt.Errorf("failed getting user info: %s", err.Error())
    }
    defer response.Body.Close()
    contents, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("failed read response: %s", err.Error())
    }
    return contents, nil
}


Enter fullscreen mode Exit fullscreen mode

完整代码 oauth_google.go

package handlers

import (
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
    "net/http"
    "fmt"
    "io/ioutil"
    "context"
    "log"
    "encoding/base64"
    "crypto/rand"
    "os"
    "time"
)

// Scopes: OAuth 2.0 scopes provide a way to limit the amount of access that is granted to an access token.
var googleOauthConfig = &oauth2.Config{
    RedirectURL:  "http://localhost:8000/auth/google/callback",
    ClientID:     os.Getenv("GOOGLE_OAUTH_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"),
    Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email"},
    Endpoint:     google.Endpoint,
}

const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="

func oauthGoogleLogin(w http.ResponseWriter, r *http.Request) {

    // Create oauthState cookie
    oauthState := generateStateOauthCookie(w)

    /*
    AuthCodeURL receive state that is a token to protect the user from CSRF attacks. You must always provide a non-empty string and
    validate that it matches the the state query parameter on your redirect callback.
    */
    u := googleOauthConfig.AuthCodeURL(oauthState)
    http.Redirect(w, r, u, http.StatusTemporaryRedirect)
}

func oauthGoogleCallback(w http.ResponseWriter, r *http.Request) {
    // Read oauthState from Cookie
    oauthState, _ := r.Cookie("oauthstate")

    if r.FormValue("state") != oauthState.Value {
        log.Println("invalid oauth google state")
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    data, err := getUserDataFromGoogle(r.FormValue("code"))
    if err != nil {
        log.Println(err.Error())
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    // GetOrCreate User in your db.
    // Redirect or response with a token.
    // More code .....
    fmt.Fprintf(w, "UserInfo: %s\n", data)
}

func generateStateOauthCookie(w http.ResponseWriter) string {
    var expiration = time.Now().Add(365 * 24 * time.Hour)

    b := make([]byte, 16)
    rand.Read(b)
    state := base64.URLEncoding.EncodeToString(b)
    cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration}
    http.SetCookie(w, &cookie)

    return state
}

func getUserDataFromGoogle(code string) ([]byte, error) {
    // Use code to get token and get user info from Google.

    token, err := googleOauthConfig.Exchange(context.Background(), code)
    if err != nil {
        return nil, fmt.Errorf("code exchange wrong: %s", err.Error())
    }
    response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
    if err != nil {
        return nil, fmt.Errorf("failed getting user info: %s", err.Error())
    }
    defer response.Body.Close()
    contents, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("failed read response: %s", err.Error())
    }
    return contents, nil
}

Enter fullscreen mode Exit fullscreen mode

让我们运行并测试一下

go run main.go
Enter fullscreen mode Exit fullscreen mode

带有代码库的存储

文章来源:https://dev.to/douglasmakey/oauth2-example-with-go-3n8a
PREV
体验微服务架构和通信
NEXT
.NET 团队最喜欢的 Razor 功能 TagHelperPack BlazorWebFormsComponents