您好,在今天的文章中,我将向您展示如何构建自己的OAuth2服务器,就像 google ,facebook, github 等。
如果您想构建生产就绪的公共或私有API,这将非常有用。所以让我们开始吧。
什么是OAuth2?
Open Authorization Version 2.0称为OAuth2。它是一种保护RESTful Web服务的协议或框架。OAuth2非常强大。现在,大多数REST API都受到OAuth2的保护,因为它具有坚固的安全性。
OAuth2有两个部分
01.客户
02.服务器
OAuth2客户端

如果你熟悉这个屏幕,你知道我在说什么。无论如何,让我解释一下图像背后的故事:
您正在构建面向用户的应用程序,该应用程序与用户的github存储库一起使用。例如:CI工具,如TravisCI,CircleCI,Drone等。
但是用户的github帐户是安全的,如果所有者不想要,则没有人可以访问它。那么这些CI工具如何访问用户的github帐户和存储库呢?
简单。
您的应用程序将询问用户
然后用户会说
“是的,我愿意。并做任何需要做的事情。
然后,您的应用程序将联系github的权限,以授予对该特定用户的github帐户的访问权限。Github将检查它是否属实并要求该用户进行授权。然后github将向客户端发出一个短暂的令牌。
现在,当您的应用程序需要在身份验证和授权后访问它时,它需要发送带有请求的访问令牌,以便github会认为:
“哦,访问令牌看起来很熟悉,可能是我们已经给你了。好的,你可以访问“
那是漫长的故事。天已经改变,现在你不需要每次都去github权限(我们从来没有这样做过)。一切都可以自动完成。
但是怎么样?

这是我几分钟前谈过的UML序列图。只是图形表示。
从上图中,我们发现了一些重要的事情。
OAuth2有4个角色:
01.用户 – 将使用您的应用程序的最终用户
02.客户端 – 您正在构建的应用程序将使用github帐户并且用户将使用该应用程序
- Auth Server – 处理主要OAuth事务的服务器
04.资源服务器 – 具有受保护资源的服务器。例如github
客户端代表用户向auth服务器发送OAuth2请求。
构建OAuth2客户端既不容易也不困难。听起来很有趣,对吗?我们将在下一部分中做到这一点。
但在这一部分,我们将走向世界的另一端。我们将构建自己的OAuth2服务器。哪个不容易但多汁。
准备?我们走吧
OAuth2服务器
你可能会问我
“等一下Cyan,为什么要建一个OAuth2服务器?”
你忘记了吗?我早些时候已经说过了。好的,我再告诉你。
想象一下,您正在构建一个非常有用的应用程序,可以提供准确的天气信息(这里有很多这样的api)。现在你想让它打开,以便公众可以使用它,或者你想用它赚钱。
无论是什么情况,您都需要保护您的资源免受未经授权的访问或恶意攻击。为此,您需要保护您的API资源。这是OAuth2的事情。答对了!
从上图中,我们可以看到我们需要在REST API资源服务器前放置一个Auth服务器。这就是我们所说的。Auth服务器将使用OAuth2规范构建。然后我们将成为第一张照片的github,哈哈哈开玩笑。
OAuth2服务器的主要目标是为客户端提供访问令牌。这就是为什么OAuth2 Server也称为OAuth2 Provider,因为它们提供令牌。
够说话了。
基于授权流类型有四种类型的OAuth2服务器:
01.授权代码授权
02.隐式授予
03.客户证书授予
04.密码授予
如果您想了解有关OAuth2的更多信息,请查看 这篇 精彩的文章。
对于本文,我们将使用 客户端凭据授予类型 。所以让我们深入研究
客户端凭据授予基于流的服务器
在实施基于客户端凭据授权流程的OAuth2服务器时,我们需要了解一些事情。
在此授权类型中,没有用户交互(即注册,登录)。需要两件事,它们是 client_id 和 client_secret 。有了这两件事,我们就可以获得 access_token了 。客户是第三方应用程序。当您需要在没有用户或仅由客户端应用程序访问资源服务器时,此授权类型很简单且最适合。

这是它的UML序列图。
编码
为了构建这个,我们需要依赖一个很棒的Go包
首先,让我们构建一个简单的API服务器作为资源服务器
main.go
package main import ( "log" "net/http" ) func main() { http.HandleFunc("/ protected ", func(w http.ResponseWriter, r *http. Request ) { w.Write([] byte ("Hello, I'm protected")) }) log.Fatal(http.ListenAndServe(":9096", nil ))
运行服务器并将获取请求发送到
你会得到回应。
它是什么样的受保护服务器?
虽然端点名称受到保护,但任何人都可以访问它。所以我们需要用OAuth2来保护它。
现在我们将编写授权服务器
路线
- /credentials 用于发出客户机凭据(client_id和client_secret)
- / token 发出带有客户端凭据的令牌
我们需要实现这两条路线。
这是初步设置
main.go
package main
import (
"encoding/ json "
"fmt"
"github.com/google/uuid"
"gopkg.in/oauth2.v3/models"
"log"
"net/http"
"time"
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
)
func main() {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
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("/protected", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
})
log.Fatal(http.ListenAndServe(":9096", nil))
}
在这里,我们创建了一个管理器,客户端存储和auth服务器本身。
这是 /credentials 路由
http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "",
})
if err != nil {
fmt.Println(err.Error())
}
w. Header ().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
它创建了两个随机字符串,一个用于client_id,另一个用于client_secret。然后将它们保存到客户端存储中。并将它们作为回应返回。我们可以使用redis,mongodb,postgres等内存存储数据库,将它们存储在起来。
这是 / token 路由:
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) })
这很简单。它将请求和响应传递给适当的处理程序,以便服务器可以解码请求有效负载中的所有必要数据。
所以这是我们的整体代码:
package main import ( "encoding/json" "fmt" "github.com/google/uuid" "gopkg.in/oauth2.v3/models" "log" "net/http" "time" "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" ) func main() { manager := manage.NewDefaultManager() manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) manager.MustTokenStorage(store.NewMemoryTokenStore()) clientStore := store.NewClientStore() manager.MapClientStorage(clientStore) srv := server.NewDefaultServer(manager) srv.SetAllowGetAccessRequest(true) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) 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("/token", func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) }) http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) { clientId := uuid.New().String()[:8] clientSecret := uuid.New().String()[:8] err := clientStore.Set(clientId, &models.Client{ ID: clientId, Secret: clientSecret, Domain: "", }) if err != nil { fmt.Println(err.Error()) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret}) }) http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, I'm protected")) }) log.Fatal(http.ListenAndServe(":9096", nil)) }
运行代码并转到 route以注册并获取client_id和client_secret
现在转到此URL http:// localhost:9096/token?grant_type=client_credentials&client_id = YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope = all
您将获得具有到期时间和其他一些信息的access_token。
现在我们得到了access_token。但是我们/受保护的路线仍然没有受到保护。我们需要设置一种方法来检查每个客户端请求是否存在有效令牌。如果是,那么我们给客户端访问权限。否则不是。
我们可以用中间件来做到这一点。
如果您知道自己在做什么,那么在go中编写中间件会非常有趣。这是中间件:
func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := srv.ValidationBearerToken(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f.ServeHTTP(w, r) }) }
这将检查是否为请求提供了有效的令牌,并根据该令牌采取行动。
现在我们需要配置/protected路由
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, I'm protected")) }, srv))
现在整个代码看起来像这样:
package main import ( "encoding/json" "fmt" "github.com/google/uuid" "gopkg.in/oauth2.v3/models" "log" "net/http" "time" "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" ) func main() { manager := manage.NewDefaultManager() manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) // token memory store manager.MustTokenStorage(store.NewMemoryTokenStore()) // client memory store clientStore := store.NewClientStore() manager.MapClientStorage(clientStore) srv := server.NewDefaultServer(manager) srv.SetAllowGetAccessRequest(true) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) 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("/token", func(w http.ResponseWriter, r *http.Request) { srv.HandleTokenRequest(w, r) }) http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) { clientId := uuid.New().String()[:8] clientSecret := uuid.New().String()[:8] err := clientStore.Set(clientId, &models.Client{ ID: clientId, Secret: clientSecret, Domain: "", }) if err != nil { fmt.Println(err.Error()) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret}) }) http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, I'm protected")) }, srv)) log.Fatal(http.ListenAndServe(":9096", nil)) } func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := srv.ValidationBearerToken(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } f.ServeHTTP(w, r) }) }
现在运行服务器并尝试访问 / protected 端点,而不将 access_token 作为URL Query。然后尝试给出错误的 access_token 。无论哪种方式,auth服务器都会阻止你。
现在再次从服务器获取 凭据 和 access_token ,并将请求发送到受保护的端点:
HTTP://localhost:9096/test?access_token = YOUR_ACCESS_TOKEN
答对了!你可以访问它。
所以我们已经学会了如何使用Go设置我们自己的OAuth2服务器。
在下一部分中,我们将在Go中构建OAuth2客户端。在最后一部分中,我们将使用用户登录和授权构建 授权代码授予类型的服务器 。
译: