[译] 在 GO 语言中建立你本身的 OAuth2 服务:客户端凭据受权流程

嗨,在今天的文章中,我会向你们展现怎么构建属于每一个人本身的 OAuth2 服务器,就像 google、facebook 和 github 等公司同样。前端

若是你想构建用于生产环境的公共或者私有 API,这都会是颇有帮助的。因此如今让咱们开始吧。android

什么是 OAuth2?

开放受权版本 2.0 被称为 OAuth2。它是一种保护 RESTful Web 服务的协议或者说是框架。OAuth2 很是强大。因为 OAuth2 坚如磐石的安全性,因此如今大多数的 REST API 都经过 OAuth2 进行保护。ios

OAuth2 具备两个部分

  1. 客户端git

  2. 服务端github

OAuth2 客户端

若是你熟悉这个界面,你就会知道我将要说什么。可是不管熟悉与否,都让我来说一下这个图片背后的故事吧。golang

你正在构建一个面向用户的应用程序,它是与用户的 github 仓库协同使用的。好比:就像是 TravisCI、CircleCI 和 Drone 等 CI 工具。redis

可是用户的 github 帐户是被保护的,若是全部者不肯意任何人都无权访问。那么这些 CI 工具如何访问用户的 github 账户和仓库的呢?mongodb

这其实很简单。json

你的应用程序会询问用户后端

“为了与咱们的服务协做,咱们须要获得你的 github 仓库的读取权限。你赞成吗?”

而后这个用户就会说

“我赞成。大家能够去作大家须要作的事儿啦。"

而后你的应用程序会请求 github 的权限管理以得到那个特定用户的 github 访问权限。Github 会检查是否属实并要求该用户进行受权。经过以后 github 就会给这个客户端发送一个临时的令牌。

如今,当你的应用程序获得身份验证和受权之后须要访问 github 时,就须要把这个令牌在请求中间带过去,github 收到了以后就会想:

“咦,这个访问令牌看起来很眼熟嘛,应该是咱们以前就给过你了。好,你能够访问了”

这是一个很长的流程。可是时代已经变啦,如今你不用每次都去 github 受权中心(固然咱们历来也不须要这样)。每件事均可以自动化地完成。

可是怎么完成呢?

这是我前几分钟讨论的内容所对应的 UML 时序图。就是一个对应的图形表示。

从上图中,咱们能够发现几点重要的东西。

OAuth2 有 4 个角色:

  1. 用户 — 最终使用你的应用程序的用户

  2. 客户端 — 就是你构建的那个会使用 github 帐户的应用程序,也就是用户会使用的东西

  3. 鉴权服务器 — 这个服务器主要处理 OAuth 相关事务

  4. 资源服务器 — 这个服务器有那些被保护的资源。好比说 github

客户端表明用户向鉴权服务器发送 OAuth2 请求。

构建一个 OAuth2 客户端不算简单但也不算困难。听起来颇有趣对吧?咱们会在下一个部分来实际操做。

但在这个部分,咱们会去这个世界的另外一面看看。咱们会构建咱们本身的 OAuth2 服务端。这并不简单可是颇有趣。

准备好了吗?让咱们开始吧

OAuth2 服务端

你也许会问我

“Cyan 等一下,为何要构建一个 OAuth2 服务器啊?”

朋友你忘了吗?我以前说了这一点的啊。好吧,让我再次告诉你。

想象一下,你构建了一个很是棒的应用程序,它能够提供准确的天气信息(如今已经有不少这种类型的 API 了)。如今你但愿把它变得开放让公众均可以使用或者你想靠它来赚钱了。

但不管什么状况,你都须要保护你的资源免受未经受权的访问或者恶意的攻击。 因此你须要保护你的 API 资源。那这里就须要用到 OAuth2 啦。对吧!

从上图中咱们能够看到,鉴权服务器须要放置在 REST API 资源服务器以前。这就是咱们要讨论的东西。这个鉴权服务器须要根据 OAuth2 规范构建。而后咱们就会变成第一张图片里面的 github 啦,哈哈哈哈开玩笑的。

OAuth2 服务器的主要目标是给客户端提供访问的令牌。这也就是为何 OAuth2 服务器也被称做 OAuth2 提供者,由于他们能够提供令牌。

这个解释就说这么多啦。

基于鉴权流程有 4 种不一样的 OAuth2 服务器模式:

  1. 受权码模式

  2. 隐式受权模式

  3. 客户端验证模式

  4. 密码模式

若是你想了解更多关于 OAuth2 的东西,请看 这里的 精彩文章。

在本文中,咱们会使用 客户端验证模式。我们来深刻了解一下吧。

基于服务器的客户端凭据受权流程

在构建基于 OAuth2 服务器的客户端凭据受权流程时,咱们须要了解一些东西。

在这个受权类型里面没有用户交互 (也就是指没有注册,登陆)。而是须要两个东西,它们是 客户端 ID客户端密钥。有了这两个东西,咱们就能够获取到 访问令牌。客户端就是第三方的应用程序。当须要在没有用户机制或者是仅经过客户端应用程序,想要访问资源服务器的时候,这种受权方式是简便且适合的。

这就是对应的 UML 时序图。

编码

为了构建这个项目,咱们须要依赖一个很是棒的 Go 语言包。

首先,咱们须要开发一个简单的 API 服务做为资源服务器。

package main

import (
	"log"
	"net/http"
)

func main() {
	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))
}
复制代码

运行这个服务而且发送 Get 请求到 http://localhost:9096/protected

你会获得响应。

这个服务受到什么类型的保护呢?

即便将这个接口的名字定义为 protected,可是任何人均可以请求它。咱们须要将这个接口使用 OAuth2 保护。

如今咱们就要编写咱们本身的受权服务。

路由

  1. /credentials 用于颁发客户端凭据 (客户端 ID 和客户端密钥)

  2. /token 使用客户端凭据颁发令牌

咱们须要实现这两个路由。

这里是初步的设置

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))
}
复制代码

这里咱们建立了一个管理器,用于客户端存储和鉴权服务自己。

这里是 /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: "http://localhost:9094",
   })
   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})
})
复制代码

它建立了两个随机字符串,一个就是客户端 ID,另外一个就是客户端密钥。并把它们保存到客户端存储。而后就会返回响应。就是这样。在这里咱们使用了内存存储,但咱们一样能够把它们存储到 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: "http://localhost:9094",
      })
      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))
}
复制代码

运行这个代码并到 http://localhost:9096/credentials 路由去注册并获取客户端 ID 和客户端密钥。

如今去到这个连接 http://localhost:9096/token?grant_type=client_credentials&client_id=2e14f7dd&client_secret=c729e9d0&scope=all

你能够获得具备过时时间和一些其余信息的受权令牌。

如今咱们获得了咱们的受权令牌。可是咱们的 /protected 路由依然没有被保护。咱们须要设置一个方法来检查每一个客户端的请求是否都带有有效的令牌。若是是的,咱们就能够给予这个客户端受权。反之就不能给予受权。

咱们能够经过一个中间件来作到这一点。

若是你知道你在作什么,那么在 golang 中编写中间件会颇有趣。如下就是中间件的代码:

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: "http://localhost:9094",
      })
      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)
   })
}
复制代码

如今运行服务并在 URL 不带有 访问令牌 的状况下访问 /protected 接口。或者尝试使用错误的 访问令牌。在这两种方式下鉴权服务都会阻止你。

如今再次从服务器得到认证信息 and 访问令牌 并发送请求到受保护的接口:

http://localhost:9096/test?access_token=YOUR_ACCESS_TOKEN

对啦!你如今有权限访问啦。

如今咱们已经学会了怎么使用 Go 来设置咱们本身的 OAuth2 服务器。

在下一部分中。咱们会在 Go 中构建咱们本身的 OAuth2 客户端。而且在最后一部分,咱们会基于登陆和受权构建咱们本身的 基于服务器的受权码模式

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索