跳转到主要内容

欢迎各位码农!在本教程中,我们将了解如何使用 go-oauth2/oauth2 包实现自己的 OAuth2 服务器和客户端。

毫无疑问,这是评论者对我的 YouTube 视频提出的最多要求的话题之一,而且我自己也觉得非常有趣。

毫无疑问,对于任何面向公共甚至私有的服务或 API 而言,安全性都是一个非常重要的特性,并且您需要非常注意它才能使其正确。

注意 - 可以在此处找到本教程的完整 github 存储库:TutorialEdge/go-oauth-tutorial

理论


因此,在我们深入研究如何编写代码之前,了解它在后台是如何工作的很重要。通常,我们有一个客户端,它首先向资源所有者发出授权请求。然后,资源所有者要么同意要么拒绝这个请求。

使用此授权授予,客户端然后将其传递给授权服务器,授权服务器将授予访问令牌。正是有了这个授予的访问令牌,我们的客户端才能访问受保护的资源,例如 API 或服务。

话虽如此,现在让我们看看如何使用这个 go-oauth2/oauth2 包来实现我们自己的授权服务器。

注意 - 如果您有兴趣查看 Oauth2 实现遵循的 RFC,可以在此处找到:RFC-6749

一个简单的 Oauth2 流程


我们将从基于他们在文档中提供的示例实现一个非常简单的服务器开始。当我们将客户端 ID 和客户端密码传递给授权服务器时,它应该返回我们的访问令牌,如下所示:

{"access_token":"Z_1QUVC5M_EOCESISKW8AQ","expires_in":7200,"scope":"read","token_type":"Bearer"}


所以,让我们深入研究一下我们的服务器实现,看看我们是否可以破译发生了什么:

 

package main

import (
    "log"
    "net/http"
    "net/url"
    "os"

    "github.com/go-session/session"
    "gopkg.in/oauth2.v3/errors"
    "gopkg.in/oauth2.v3/manage"
    "gopkg.in/oauth2.v3/models"
    "gopkg.in/oauth2.v3/server"
    "gopkg.in/oauth2.v3/store"
)

func main() {
    manager := manage.NewDefaultManager()
    // token store
    manager.MustTokenStorage(store.NewMemoryTokenStore())

    clientStore := store.NewClientStore()
    clientStore.Set("222222", &models.Client{
        ID:     "222222",
        Secret: "22222222",
        Domain: "http://localhost:9094",
    })
    manager.MapClientStorage(clientStore)

    srv := server.NewServer(server.NewConfig(), manager)
    srv.SetUserAuthorizationHandler(userAuthorizeHandler)

    srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
        log.Println("Internal Error:", err.Error())
        return
    })

    srv.SetResponseErrorHandler(func(re *errors.Response) {
        log.Println("Response Error:", re.Error.Error())
    })

    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/auth", authHandler)

    http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
        err := srv.HandleAuthorizeRequest(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
        }
    })

    http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
        err := srv.HandleTokenRequest(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })

    log.Println("Server is running at 9096 port.")
    log.Fatal(http.ListenAndServe(":9096", nil))
}

func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        return
    }

    uid, ok := store.Get("UserID")
    if !ok {
        if r.Form == nil {
            r.ParseForm()
        }
        store.Set("ReturnUri", r.Form)
        store.Save()

        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusFound)
        return
    }
    userID = uid.(string)
    store.Delete("UserID")
    store.Save()
    return
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if r.Method == "POST" {
        store.Set("LoggedInUserID", "000000")
        store.Save()

        w.Header().Set("Location", "/auth")
        w.WriteHeader(http.StatusFound)
        return
    }
    outputHTML(w, r, "static/login.html")
}

func authHandler(w http.ResponseWriter, r *http.Request) {
    store, err := session.Start(nil, w, r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if _, ok := store.Get("LoggedInUserID"); !ok {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusFound)
        return
    }

    if r.Method == "POST" {
        var form url.Values
        if v, ok := store.Get("ReturnUri"); ok {
            form = v.(url.Values)
        }
        u := new(url.URL)
        u.Path = "/authorize"
        u.RawQuery = form.Encode()
        w.Header().Set("Location", u.String())
        w.WriteHeader(http.StatusFound)
        store.Delete("Form")

        if v, ok := store.Get("LoggedInUserID"); ok {
            store.Set("UserID", v)
        }
        store.Save()

        return
    }
    outputHTML(w, r, "static/auth.html")
}

func outputHTML(w http.ResponseWriter, req *http.Request, filename string) {
    file, err := os.Open(filename)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer file.Close()
    fi, _ := file.Stat()
    http.ServeContent(w, req, file.Name(), fi.ModTime(), file)
}

我们的客户端


现在我们已经完成了我们的服务器实现并除尘,我们可以专注于构建我们的客户端。这将使用 golang.org/x/oauth2 标准包进行身份验证。

我们将使用具有 2 个端点的 net/http 定义一个非常简单的服务器:

  • / - 我们客户的根目录或主页
  • /oauth2 - 成功验证客户端的路由将自动重定向到。

我们将首先定义我们的 oauth2.Config{} 对象,该对象将包含我们的 ClientID 或 ClientSecret。我们的 OAuth2 服务器实现已经记录了这两个变量,如果它们不匹配,我们将无法从我们的服务器检索访问令牌。

它还将接受定义访问令牌范围的范围字符串,这些范围可以定义对给定资源的各种不同级别的访问。例如,我们可以定义一个只读范围,它只为客户端提供对我们底层资源的只读访问权限。

接下来,我们定义了 RedirectURL,它指定了我们的授权服务器在成功认证后应该重定向到的端点。我们希望这由我们的 /oauth2 端点处理。

最后,我们指定 oauth2.Endpoint ,它接受 AuthURL 和 TokenURL ,它们将指向我们之前在服务器上定义的授权和令牌端点。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "golang.org/x/oauth2"
)

var (
    config = oauth2.Config{
        ClientID:     "222222",
        ClientSecret: "22222222",
        Scopes:       []string{"all"},
        RedirectURL:  "http://localhost:9094/oauth2",
        // This points to our Authorization Server
        // if our Client ID and Client Secret are valid
        // it will attempt to authorize our user
        Endpoint: oauth2.Endpoint{
            AuthURL:  "http://localhost:9096/authorize",
            TokenURL: "http://localhost:9096/token",
        },
    }
)

// Homepage
func HomePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Homepage Hit!")
    u := config.AuthCodeURL("xyz")
    http.Redirect(w, r, u, http.StatusFound)
}

// Authorize
func Authorize(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    state := r.Form.Get("state")
    if state != "xyz" {
        http.Error(w, "State invalid", http.StatusBadRequest)
        return
    }

    code := r.Form.Get("code")
    if code == "" {
        http.Error(w, "Code not found", http.StatusBadRequest)
        return
    }

    token, err := config.Exchange(context.Background(), code)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    e := json.NewEncoder(w)
    e.SetIndent("", "  ")
    e.Encode(*token)
}

func main() {

    // 1 - We attempt to hit our Homepage route
    // if we attempt to hit this unauthenticated, it
    // will automatically redirect to our Auth
    // server and prompt for login credentials
    http.HandleFunc("/", HomePage)

    // 2 - This displays our state, code and
    // token and expiry time that we get back
    // from our Authorization server
    http.HandleFunc("/oauth2", Authorize)

    // 3 - We start up our Client on port 9094
    log.Println("Client is running at 9094 port.")
    log.Fatal(http.ListenAndServe(":9094", nil))
}

所以,我们已经成功地建立了我们的客户。让我们尝试运行它,看看会发生什么。

$ go run main.go
2018/10/20 13:25:22 Client is running at 9094 port.


现在,每当您在浏览器中点击 localhost:9094 时,您应该会看到它自动重定向到您正在运行的服务器实现 localhost:9096/login。然后,我们将提供凭据 admin 和 admin 用于演示目的,这将提示我们授予客户端访问权限。

当我们点击允许时,它会自动将我们重定向回我们的客户端应用程序 /oauth2 端点,但它会返回一个 JSON 字符串,其中包含我们的 access_token、refresh_token、token_type 以及我们的令牌何时到期。

太棒了,我们实现了一个完整的 Oauth2 流程。

结论


因此,在本教程中,我们研究了如何在 Go 中实现自己的授权服务器。然后,我们研究了如何构建一个简单的基于 Go 的客户端,该客户端随后可以向该服务器发出访问令牌的请求。

希望您发现本教程很有用!如果你这样做了,请随时在下面的评论部分告诉我!

 

文章链接