使用Golang构建你的OAuth2服务

您好,在今天的文章中,我将向您展示如何构建自己的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会认为:

“哦,访问令牌看起来很熟悉,可能是我们已经给你了。好的,你可以访问“

那是漫长的故事。天已经改变,现在你不需要每次都去github权限(我们从来没有这样做过)。一切都可以自动完成。

但是怎么样?

image.png

这是我几分钟前谈过的UML序列图。只是图形表示。

从上图中,我们发现了一些重要的事情。

OAuth2有4个角色:

01.用户 - 将使用您的应用程序的最终用户

02.客户端 - 您正在构建的应用程序将使用github帐户并且用户将使用该应用程序

  1. 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_idclient_secret。有了这两件事,我们就可以获得access_token了。客户是第三方应用程序。当您需要在没有用户或仅由客户端应用程序访问资源服务器时,此授权类型很简单且最适合。

image.png

这是它的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))

运行服务器并将获取请求发送到http://localhost:9096/protected

你会得到回应。

它是什么样的受保护服务器?

虽然端点名称受到保护,但任何人都可以访问它。所以我们需要用OAuth2来保护它。

现在我们将编写授权服务器

路线

  1. /credentials用于发出客户机凭据(client_id和client_secret)

  2. / 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: "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})
})

它创建了两个随机字符串,一个用于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: "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 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: "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)
   })
}

现在运行服务器并尝试访问/ protected端点,而不将access_token作为URL Query。然后尝试给出错误的access_token。无论哪种方式,auth服务器都会阻止你。

现在再次从服务器获取凭据access_token,并将请求发送到受保护的端点:

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

答对了!你可以访问它。

所以我们已经学会了如何使用Go设置我们自己的OAuth2服务器。

在下一部分中,我们将在Go中构建OAuth2客户端。在最后一部分中,我们将使用用户登录和授权构建授权代码授予类型的服务器

转自:https://hackernoon.com/build-your-own-oauth2-server-in-go-7d0f660732c3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容