Skip to content

Latest commit

 

History

History
685 lines (524 loc) · 36 KB

File metadata and controls

685 lines (524 loc) · 36 KB

十二、处理 REST 服务的认证

在本章中,我们将探讨 Go 中的认证模式。这些模式是session-based authenticationJSON Web Tokens (JWT)Open Authentication 2 (OAuth2)。我们将尝试利用 Gorilla 包的会话库来创建基本会话。然后,我们将尝试转向高级 RESTAPI 认证策略,例如使用无状态 JWT。最后,我们将看到如何实现我们自己的 OAuth2,还将了解哪些包可以为我们提供开箱即用的 OAuth2 实现。在上一章中,API 网关为我们实现了认证(使用插件)。如果我们的体系结构中没有 API 网关,我们如何保护 API?你将在本章中找到答案。

在本章中,我们将介绍以下主题:

  • 认证的工作原理
  • 介绍 Postman,一个用于测试 API 的可视化客户端
  • Go 中基于会话的认证
  • 引入 Redis 存储用户会话
  • JSON Web 令牌(JWT)简介
  • OAuth2 体系结构和基础

获取代码

您可以在获取本章的代码示例 https://github.com/narenaryan/gorestful/tree/master/chapter12 。由于示例程序不是包,读者需要按照 GOPATH 编写项目的方式创建项目文件。

认证的工作原理

传统上,认证或简单认证是以会话为中心的。从服务器请求资源的客户端试图证明它是任何给定资源的正确使用者。流程是这样开始的。客户端使用用户凭据向服务器发送认证请求。服务器获取这些凭据并将其与存储在服务器上的凭据相匹配。如果匹配成功,它会在响应中写入称为 cookie 的内容。此 cookie 是一小段信息,在后续请求之间传输。网站的现代用户界面UI单页应用程序SPA)。在那里,静态 web 资产(如 HTML、JS)由 CDN 提供,以最初呈现 web 页面。从下一次开始,web 页面和应用程序服务器之间的通信仅通过 RESTAPI/web 服务进行。

会话是记录给定时间段内用户通信的好方法。会话是一个通常存储在 cookie 中的概念。下图总结了整个认证过程(简单的认证):

现在看看实际的方法。客户端(例如浏览器)向服务器的登录 API发送请求。服务器尝试使用数据库检查这些凭据,如果存在凭据,则将 cookie 写回响应,表示该用户已通过认证。cookie 是服务器在稍后时间点使用的消息。当客户端接收到响应时,它将该 cookie 存储在本地。如果 web 浏览器是客户端,它将其存储在 cookie 存储中。从下一次开始,客户端可以通过显示 cookie 作为通道的密钥,自由地向服务器请求资源。当客户端决定终止会话时,它会调用服务器上的注销 API。服务器在响应中销毁会话。这一进程仍在继续。服务器还可以在 cookie 上保持过期状态,以便在没有活动的情况下,认证窗口在特定时间内有效。这就是所有网站的工作方式。

现在,我们将尝试使用 Gorilla kit 的sessions包实现一个这样的系统。在最初的章节中,我们已经看到了 Gorilla 工具包如何提供 HTTP 路由器。此会话包就是其中之一。我们需要首先使用以下命令安装软件包:

go get github.com/gorilla/sessions

现在,我们可以使用以下语句创建新会话:

var store = sessions.NewCookieStore([]byte("secret_key"))

secret_key应该是 Gorilla 会话用来加密会话 cookie 的密钥。如果我们添加一个会话作为普通文本,任何人都可以阅读它。因此,服务器需要将消息加密为随机字符串。为此,它要求提供一个密钥。此密钥可以是任意随机生成的字符串。在代码中保存密钥不是一个好主意,所以我们尝试将其存储为环境变量,并在代码中动态读取。我们将看看如何实施这样一个系统。

基于会话的认证

在 GOPATH 中创建一个名为simpleAuth的项目,并添加一个main.go文件**,该文件保存了我们程序的逻辑:**

**```go mkdir simpleAuth touch main.py


在这个程序中,我们将看到如何使用 Gorilla 会话包创建基于会话的认证。请参阅以下代码段:

```go
package main
import (
    "log"
    "net/http"
    "os"
    "time"
    "github.com/gorilla/mux"
    "github.com/gorilla/sessions"
)
var store =
sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
var users = map[string]string{"naren": "passme", "admin": "password"}
// HealthcheckHandler returns the date and time
func HealthcheckHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    if (session.Values["authenticated"] != nil) && session.Values["authenticated"] != false {
        w.Write([]byte(time.Now().String()))
    } else {
        http.Error(w, "Forbidden", http.StatusForbidden)
    }
}
// LoginHandler validates the user credentials
func LoginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "Please pass the data as URL form encoded",
http.StatusBadRequest)
        return
    }
    username := r.PostForm.Get("username")
    password := r.PostForm.Get("password")
    if originalPassword, ok := users[username]; ok {
        if password == originalPassword {
            session.Values["authenticated"] = true
            session.Save(r, w)
        } else {
            http.Error(w, "Invalid Credentials", http.StatusUnauthorized)
            return
        }
    } else {
        http.Error(w, "User is not found", http.StatusNotFound)
        return
    }
    w.Write([]byte("Logged In successfully"))
}
// LogoutHandler removes the session
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    session.Values["authenticated"] = false
    session.Save(r, w)
    w.Write([]byte(""))
}
func main() {
    r := mux.NewRouter()
    r.HandleFunc("/login", LoginHandler)
    r.HandleFunc("/healthcheck", HealthcheckHandler)
    r.HandleFunc("/logout", LogoutHandler)
    http.Handle("/", r)
    srv := &http.Server{
        Handler: r,
        Addr: "127.0.0.1:8000",
        // Good practice: enforce timeouts for servers you create!
        WriteTimeout: 15 * time.Second,
        ReadTimeout: 15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

它是一个 RESTAPI,允许用户访问系统的运行状况(启动或不启动)。为了进行认证,需要首先调用登录端点。该程序从 Gorilla 工具包中导入了两个名为 mux 和 sessions 的主要软件包。Mux 用于将 HTTP 请求的 URL 端点链接到函数处理程序,会话用于动态创建新会话和验证现有会话

在 Go 中,我们需要将会话存储在程序内存中。我们可以通过创建CookieStore.来实现这一点,这一行明确告诉程序通过从名为SESSION_SECRET的环境变量中选取密钥来创建一个:

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

sessions有一个名为NewCookieStore的新函数返回一个存储。我们需要使用此存储来管理 Cookie。我们可以使用此语句获得 cookie 会话。如果会话不存在,则返回空会话:

**```go session, _ := store.Get(r, "session.id")


`session.id`是我们为会话指定的自定义名称。使用此名称,将在客户端响应中发回 cookie。`LoginHandler`**尝试将客户端提供的表单作为多部分表单数据进行解析。此步骤在程序中至关重要:**

 **```go
err := r.ParseForm()

这将使用解析的键值对填充r.PostForm映射。该 API 需要用户名和密码才能进行认证。因此,我们感兴趣的是抓取usernamepassword,一旦LoginHandler收到数据,它会尝试在一个名为用户**的地图上查看详细信息。在实际场景中,我们使用数据库验证这些细节。为了简单起见,我们硬编码了值,并尝试从中进行认证。如果用户名不存在,则返回一个错误,说明找不到资源。如果用户名存在且密码不正确,则返回UnAuthorized错误消息。如果一切顺利,通过设置 cookie 值返回 200 响应,如下所示:**

****```go session.Values["authenticated"] = true session.Save(r, w)


第一条语句将名为`"authenticated"`的 cookie 键设置为`true`。第二条语句实际上将会话保存在响应上。它将请求和响应编写器作为参数。如果删除此语句,cookie 将不会产生任何效果。现在,来到`HealthCheckHandler`,它最初做的事情与`LoginHandler`相同,如下所示:

```go
session, _ := store.Get(r, "session.id")

然后,它检查给定请求是否有一个 cookie,该 cookie 的密钥名为"authenticated"。如果该密钥存在且为 true,则表示该用户是服务器先前进行认证的用户。但是,如果该密钥不存在或者"authenticated"值为false,则会话无效,因此返回StatusForbidden错误

客户端应该有办法使登录会话无效。它可以通过调用服务器的注销 API来实现。API 只是将"authenticated"值设置为false。这会告诉服务器客户端未经过认证:

**```go session, _ := store.Get(r, "session.id") session.Values["authenticated"] = false session.Save(r, w)


通过这种方式,可以使用任何编程语言(包括 Go)中的会话来实现简单的认证

不要忘记添加此语句,因为它是修改和保存 cookie 的实际语句:`session.Save(r, w)`。

现在,让我们看看这个程序的执行情况。我们可以使用一个叫做 Postman 的很棒的工具来代替 CURL。主要的好处是它可以在包括微软视窗在内的所有平台上运行;不再需要卷发了

错误代码可能意味着不同的事情。例如,当用户试图在没有认证的情况下访问资源时,发出禁止(403),而当服务器上不存在给定资源时,发出未找到资源(404)。

# 介绍用于测试 RESTAPI 的工具 Postman

Postman 是一个很棒的工具,它允许 Windows、macOS X 和 Linux 用户发出 HTTP API 请求。您可以在[下载 https://www.getpostman.com/](https://www.getpostman.com/) 。

安装 Postman 后,在输入请求 URL**输入文本中输入 URL。选择请求的类型(`GET`、`POST`等)。对于每个请求,我们可以有许多设置,例如标题、`POST`正文和其他详细信息。请浏览邮递员文档以了解更多详细信息。邮递员的基本用法很简单。请看以下屏幕截图:**

 **![](img/0e02ffe3-9719-4ff1-9d82-832c209ba65d.png)

生成器是我们可以添加/编辑请求的窗口。前面的屏幕截图显示了我们尝试发出请求的空构建器。运行前面`simpleAuth`项目中的`main.go`并尝试调用健康检查 API,如下所示。单击发送按钮,您将看到禁止响应:

![](img/42a5cf72-dea0-41d6-b3a2-917401543995.png)

这是因为我们还没有登录。一旦认证成功,邮递员会自动保存 cookie。现在,通过将方法类型从`GET`更改为 POST 并将 URL 更改为`http://localhost:8000/login`来调用登录 API。我们还应该将 auth 细节作为多部分表单数据传递。它看起来像下面的屏幕截图:

![](img/cb63a0a0-deb2-444c-a841-d642d390a511.png)

现在,如果我们点击 send,它将验证并接收 cookie。它返回一条消息,说明登录成功。我们也可以通过点击右边发送和保存按钮下方的 cookies 链接来检查 cookies。它显示保存的 cookie 列表,您将在其中找到一个名为`session.id`的本地主机 cookie。内容如下所示:

```go
session.id=MTUwODYzNDcwN3xEdi1CQkFFQ180SUFBUkFCRUFBQUpmLUNBQUVHYzNSeWFXNW5EQThBRFdGMWRHaGxiblJwWTJGMFpXUUVZbTl2YkFJQ0FBRT189iF-ruBQmyTdtAOaMR-Rr9lNtsf1OJgirBDkcBpdEa0=; path=/; domain=localhost; Expires=Tue Nov 21 2017 01:11:47 GMT+0530 (IST);

再次尝试调用 health check API,它会返回系统日期和时间:

2017-10-22 06:54:36.464214959 +0530 IST

如果客户端向注销 API 发出GET请求:

http://localhost:8000/logout

会话将无效,并且在完成另一个登录请求之前,将禁止访问资源。

使用 Redis 持久化客户端会话

到目前为止,我们创建的会话存储在程序内存中。这意味着如果程序崩溃或重新启动,所有记录的会话都将丢失。它需要客户端再次进行认证以获得新的会话 cookie。有时这是一件令人讨厌的事情。为了在某个地方保存会话,我们选择了Redis。Redis 是一种非常快的键值存储,因为它位于主内存中。

Redis 服务器存储我们提供的任何键值对。它提供基本数据类型,如字符串、列表、哈希、集合等。欲了解更多详情,请访问https://redis.io/topics/data-types 。我们可以在 Ubuntu 16.04 上通过以下命令安装 Redis:

sudo apt-get install redis-server

在 macOS X 上,我们可以说:

brew install redis

对于 Windows,Redis 网站上也提供了二进制文件。安装 Redis 后,我们可以使用以下命令启动 Redis 服务器:

redis-server

它在默认端口6379上启动服务器。现在,我们可以使用 rediscli(命令行工具)在其中存储任何内容。打开一个新的终端并键入redis-cli。启动 shell 后,我们可以执行 Redis 命令将数据存储和检索到用户定义的类型变量中:

[7:30:30] naren:~ $ redis-cli
127.0.0.1:6379> SET Foo  1
OK
127.0.0.1:6379> GET Foo
"1"

我们可以使用SETRedis 命令存储键值。它将值存储为字符串。如果我们尝试执行GET,它将返回字符串。我们有责任将它们转换为数字。Redis 为我们提供了方便的功能来操作这些键。例如,我们可以按如下方式递增键:

127.0.0.1:6379> INCR Foo
(integer) 2

Redis 在内部将整数视为整数。如果尝试递增非数字字符串,Redis 将抛出错误:

127.0.0.1:6379> SET name "redis"
OK
127.0.0.1:6379> INCR name
(error) ERR value is not an integer or out of range

我们为什么在这里讨论 Redis?因为我们将展示 Redis 的工作原理,并在 Redis 服务器上介绍一些基本命令。我们将把我们的项目从simpleAuth修改为simpleAuthWithRedis

在该项目中,我们使用 Redis,而不是将会话存储在程序内存中。即使程序崩溃,会话也不会丢失,因为它们保存在外部服务器中。谁为这一点编写了桥接逻辑?我们应该这样做。幸运的是,我们有一个包负责 Redis 和 Go sessions 包之间的协调。

使用以下命令安装该软件包:

go get gopkg.in/boj/redistore.v1

并创建一个新的程序,进行一些修改。在这里,我们不使用会话库,而是使用redistore包。redistore有一个名为NewRediStore的函数,该函数将 Redis 配置与密钥一起作为参数。所有其他功能保持不变。现在,在simpleAuthWithRedis目录中添加一个main.go文件:

package main
import (
    "log"
    "net/http"
    "os"
    "time"
    "github.com/gorilla/mux"
    redistore "gopkg.in/boj/redistore.v1"
)
var store, err = redistore.NewRediStore(10, "tcp", ":6379", "", []byte(os.Getenv("SESSION_SECRET")))
var users = map[string]string{"naren": "passme", "admin": "password"}
// HealthcheckHandler returns the date and time
func HealthcheckHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    if (session.Values["authenticated"] != nil) && session.Values["authenticated"] != false {
        w.Write([]byte(time.Now().String()))
    } else {
        http.Error(w, "Forbidden", http.StatusForbidden)
    }
}
// LoginHandler validates the user credentials
func LoginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "Please pass the data as URL form encoded", http.StatusBadRequest)
        return
    }
    username := r.PostForm.Get("username")
    password := r.PostForm.Get("password")
    if originalPassword, ok := users[username]; ok {
        if password == originalPassword {
            session.Values["authenticated"] = true
            session.Save(r, w)
        } else {
            http.Error(w, "Invalid Credentials", http.StatusUnauthorized)
            return
        }
    } else {
        http.Error(w, "User is not found", http.StatusNotFound)
        return
    }
    w.Write([]byte("Logged In successfully"))
}
// LogoutHandler removes the session
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    session.Options.MaxAge = -1
    session.Save(r, w)
    w.Write([]byte(""))
}
func main() {
    defer store.Close()
    r := mux.NewRouter()
    r.HandleFunc("/login", LoginHandler)
    r.HandleFunc("/healthcheck", HealthcheckHandler)
    r.HandleFunc("/logout", LogoutHandler)
    http.Handle("/", r)
    srv := &http.Server{
        Handler: r,
        Addr: "127.0.0.1:8000",
        // Good practice: enforce timeouts for servers you create!
        WriteTimeout: 15 * time.Second,
        ReadTimeout: 15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

一个有趣的变化是,我们删除了会话,而不是将其值设置为false

  session.Options.MaxAge = -1

除了会话保存在 Redis 中之外,此改进程序的工作原理与前一个程序完全相同。打开 Redis CLI 并键入此命令以获取所有可用密钥:

[15:09:48] naren:~ $ redis-cli
127.0.0.1:6379> KEYS *
1) "session_VPJ54LWRE4DNTYCLEJWAUN5SDLVW6LN6MLB26W2OB4JDT26CR2GA"
127.0.0.1:6379>

那个长的"session_VPJ54LWRE4DNTYCLEJWAUN5SDLVW6LN6MLB26W2OB4JDT26CR2GA"redistore存储的密钥。如果我们删除该密钥,客户端将自动被禁止访问资源。现在,停止正在运行的程序并重新启动它。您将看到会话没有丢失。通过这种方式,我们可以保存客户端会话。我们还可以在 SQLite 数据库上持久化会话。许多第三方软件包都是为了简化这一过程而编写的。

Redis can serve the purpose of caching for your web applications. It can store temporary data such as sessions, frequently requested user content, and so on. It is usually compared to memcached.

JSON Web 令牌(JWT)和 OAuth2 简介

以前的认证样式是普通的用户名/密码和基于会话的认证。它通过将会话保存在程序内存或 Redis/SQLite3 中来管理会话有一个限制。现代 RESTAPI 实现了基于令牌的认证。这里,令牌可以是服务器生成的任何字符串,这允许客户端通过显示令牌来访问资源。这里,令牌的计算方式使得客户端和服务器只知道如何对令牌进行编码/解码。JWT试图通过让我们创建可以传递的令牌来解决这个问题。

每当客户端将认证详细信息传递给服务器时,服务器就会生成一个令牌并将其传递回客户端。客户端将其保存在某种存储中,例如数据库或本地存储(对于浏览器)。客户端使用该令牌从服务器定义的任何 API 请求资源:

这些步骤可以更简要地概括如下:

  1. 客户端通过POST请求将用户名/密码传递给登录 API。
  2. 服务器验证详细信息,如果成功,则生成 JWT 并返回,而不是创建 cookie。客户有责任存储此令牌。
  3. 现在,客户端有了 JWT。它需要在后续的 REST API 调用中添加此选项,例如请求头中的GETPOSTPUTDELETE
  4. 服务器再次检查 JWT,如果解码成功,服务器将通过查看作为令牌一部分提供的用户名发送回数据。

JWT 确保数据从正确的客户端发送。创建令牌的技术考虑了这种逻辑。JWT 利用基于密钥的加密。

JSON web 令牌格式

我们在上一节中讨论的全部内容都是围绕 JWT 令牌进行的。我们将在这里看到它到底是什么样子以及它是如何产生的。JWT 是执行几个步骤后生成的字符串。详情如下:

  1. 通过对头 JSON 进行Base64Url编码来创建 JWT 头。
  2. 通过对有效负载 JSON 进行Base64Url编码来创建 JWT 有效负载。
  3. 通过使用密钥加密附加的头和有效负载来创建签名。
  4. JWT 字符串可以通过附加头、有效负载和签名来获得。

头是一个简单的 JSON 对象。Go 中的代码段如下所示:

`{
  "alg": "HS256",
  "typ": "JWT"
}`

"alg"是用于创建签名的算法(HMAC 和 SHA-256)的缩写。消息类型为"JWT"。这对于所有标题都是通用的。算法可能因系统而异。

有效载荷如下所示:

`{
  "sub": "1234567890",
  "username": "Indiana Jones",
  "admin": true
}`

有效载荷对象中的键称为声明。声明是一个密钥,它指定了服务器的某些特殊意义。有三种类型的索赔:

  • 公开要求
  • 私人索赔(更重要)
  • 保留索赔

保留索赔

保留声明是由 JWT 标准定义的声明。他们是:

  • iat:当时发布
  • iss:发行人名称
  • 主题文本
  • 观众姓名
  • 有效期

例如,服务器在生成令牌时,可以在有效负载中设置exp声明。然后,客户端使用该令牌访问 API 资源。服务器每次都验证令牌。过期时间过后,服务器将不再验证令牌。客户端需要通过再次登录生成新令牌。

私人索赔

私有声明是用于标识一个令牌和另一个令牌的名称。它可以用于授权。授权是一个识别提出请求的客户的过程。多租户是指在一个系统中有多个客户端。服务器可以在令牌的有效负载上设置名为username的私有声明。下次,服务器可以读回此有效负载并获取用户名,然后使用该用户名授权和自定义 API 响应。

"username": "Indiana Jones"是前面示例有效负载上的私有声明。公共声明与私有声明类似,但它们应该在 IANA JSON Web 令牌注册表中注册,以使其成为标准。我们限制使用这些。

通过执行此操作可以创建签名(这不是代码,只是一个示例):

signature = HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

它只是在 Base64URL 编码的报头和负载上执行加密算法,并带有一个秘密。这个秘密可以是任何字符串。它与我们在上一个 cookie 会话中使用的秘密完全相似。这个秘密通常保存在环境变量中并加载到程序中。

现在,我们附加编码的头、编码的有效负载和签名以获得令牌字符串:

tokenString = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature

这就是 JWT 令牌的生成方式。我们要在围棋中手动完成所有这些工作吗?不可以。在 Go 或任何其他编程语言中,有几个包可用于包装此令牌的手动创建和验证。Go 有一个很棒的、受欢迎的软件包,名为jwt-go我们将在下一节中创建一个项目,使用jwt-go签署 JWT 并对其进行验证。您可以使用以下命令安装软件包:

**```go go get github.com/dgrijalva/jwt-go


这是该项目的官方 GitHub 页面:[https://github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go) 。该软件包提供了一些功能,允许我们创建令牌。还有许多其他具有不同附加功能的软件包。您可以在[上看到所有可用的软件包和支持的功能 https://jwt.io/#libraries-io](https://jwt.io/#libraries-io)。

# 在 Go 中创建 JWT

`jwt-go`包有一个名为`NewWithClaims`的函数,它接受两个参数:

1.  签名方法,如 HMAC256、RSA 等
2.  索赔地图

例如,它看起来像以下代码段:

```go
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "username": "admin",
    "iat":time.Now().Unix(),
})

jwt.SigningMethodHS256是包内可用的加密算法。第二个参数是一个带有声明的映射,例如 private(此处为 username)和 reserved(发布于)。现在我们可以在令牌上使用SignedString函数生成tokenString

**```go tokenString, err := token.SignedString("my_secret_key")


该`tokenString`然后应传回客户端。

# 阅读围棋中的 JWT

`jwt-go`还为我们提供了解析给定 JWT 字符串的 API。`Parse`**函数采用字符串和键函数作为参数。`key`函数是一个自定义函数,用于验证算法是否正确。假设这是前面编码生成的示例令牌字符串:**

 **```go
tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoiMTUwODc0MTU5MTQ2NiJ9.5m6KkuQFCgyaGS_xcVy4xWakwDgtAG3ILGGTBgYVBmE"

我们可以使用以下方法解析并获取原始 JSON:

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    // key function
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
    }
    return "my_secret_key", nil
})

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
    // Use claims for authorization if token is valid
    fmt.Println(claims["username"], claims["iat"])
} else {
    fmt.Println(err)
}

token.Claims由一个名为MapClaims的映射实现。我们可以从该映射中获得原始 JSON 键值对。

OAuth 2 体系结构和基础

OAuth2 是一个认证框架,用于在不同系统之间创建认证模式。在这种情况下,客户端不是向资源服务器发出请求,而是向称为资源所有者的实体发出初始请求。此资源所有者返回客户端的认证授权(如果凭据成功)。客户端现在将此认证授权发送给另一个名为认证服务器的实体。此认证服务器接受授权并返回访问令牌。这个令牌是客户端访问 API 资源的关键。它需要使用此访问令牌向资源服务器发出 API 请求,并提供响应。在整个流程中,第二部分可以使用 JWT 完成。在此之前,让我们了解认证和授权之间的区别。

认证与授权

认证是识别客户是否真实的过程。当服务器对客户端进行认证时,它会检查用户名/密码对并创建会话 cookie/JWT。

授权是认证成功后区分一个客户端和另一个客户端的过程。在云服务中,客户端请求的资源需要通过检查资源是否属于该客户端而不是其他客户端来提供服务。对资源的权限和访问权限因客户端而异。例如,管理员拥有最高的资源特权。普通用户的访问是有限的。

OAuth2 是一种协议,用于对服务的多个客户端进行认证,而 JWT 是一种令牌格式。我们需要对 JWT 令牌进行编码/解码,以实现 OAuth 2 的第二阶段(以下屏幕截图中的虚线)。

请看下图:

在这个图中,我们可以使用 JWT 实现虚线部分。认证发生在认证服务器级别,授权发生在资源服务器级别。

在下一节中,让我们编写一个程序,完成两件事:

  1. 验证客户端并返回 JWT 字符串。
  2. 通过验证 JWT 来授权客户端 API 请求。

创建名为jwtauth的目录并添加main.go

**```go package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time" jwt "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" "github.com/gorilla/mux" ) var secretKey = []byte(os.Getenv("SESSION_SECRET")) var users = map[string]string{"naren": "passme", "admin": "password"} // Response is a representation of JSON response for JWT type Response struct { Token string json:"token" Status string `json:"status"` } // HealthcheckHandler returns the date and time func HealthcheckHandler(w http.ResponseWriter, r *http.Request) { tokenString, err := request.HeaderExtractor{"access_token"}.ExtractToken(r) token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") return secretKey, nil }) if err != nil { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Access Denied; Please check the access token")) return } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // If token is valid response := make(map[string]string) // response["user"] = claims["username"] response["time"] = time.Now().String() response["user"] = claims["username"].(string) responseJSON, _ := json.Marshal(response) w.Write(responseJSON) } else { w.WriteHeader(http.StatusForbidden) w.Write([]byte(err.Error())) } } // LoginHandler validates the user credentials func getTokenHandler(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Please pass the data as URL form encoded", http.StatusBadRequest) return } username := r.PostForm.Get("username") password := r.PostForm.Get("password") if originalPassword, ok := users[username]; ok { if password == originalPassword { // Create a claims map claims := jwt.MapClaims{ "username": username, "ExpiresAt": 15000, "IssuedAt": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(secretKey) if err != nil { w.WriteHeader(http.StatusBadGateway) w.Write([]byte(err.Error())) } response := Response{Token: tokenString, Status: "success"} responseJSON, _ := json.Marshal(response) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") w.Write(responseJSON) } else { http.Error(w, "Invalid Credentials", http.StatusUnauthorized) return } } else { http.Error(w, "User is not found", http.StatusNotFound) return } } func main() { r := mux.NewRouter() r.HandleFunc("/getToken", getTokenHandler) r.HandleFunc("/healthcheck", HealthcheckHandler) http.Handle("/", r) srv := &http.Server{ Handler: r, Addr: "127.0.0.1:8000", // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe()) }


这是一个需要消化的非常冗长的程序。首先,我们正在导入`jwt-go`及其子包`request`,我们正在为两个端点创建一个 RESTAPI;一个用于通过提供认证详细信息获取访问令牌,另一个用于获取授权用户的健康检查 API

在**`getTokenHandler`**处理函数中,我们将用户名和密码与自定义的用户地图进行比较。这也可以是一个数据库。如果认证成功,我们将生成一个 JWT 字符串并将其发送回客户端。

在`HealthcheckHandler`中,我们从名为`access_token`**的头中获取访问令牌,并通过解析 JWT 字符串对其进行验证。谁在写验证逻辑?JWT 包本身。当创建一个新的 JWT 字符串时,它应该有一个名为`ExpiresAt`的声明。请参阅以下代码段:**

 **```go
      claims := jwt.MapClaims{
        "username": username,
        "ExpiresAt": 15000,
        "IssuedAt": time.Now().Unix(),
      } 

程序的内部验证逻辑查看IssuedAtExpiresAt声明,并尝试计算和查看给定令牌是否过期。如果是新的,则表示令牌已验证。

现在,当令牌有效时,我们可以在HealthCheckHandler中读取有效负载,在这里我们解析作为 HTTP 请求头的一部分传递的access_token字符串。username是我们为授权插入的自定义私人索赔。因此,我们知道是谁发出了这个请求。对于每个请求,不需要通过会话。每个 API 调用都是独立的,并且基于令牌。信息编码在令牌本身中。

****token.Claims.(jwt.MapClaims) returns a map whose values are interfaces, not strings. In order to convert the value to a string, we should do claims["username"].(string).

让我们看看这个程序是如何通过邮递员工具发出请求的:

这将返回一个包含 JWT 标记的 JSON 字符串。将其复制到剪贴板。如果您尝试向运行状况检查 API 发出请求,但未将 JWT 令牌作为标头之一传递,则将收到此错误消息,而不是 JSON:

Access Denied; Please check the access token

现在,将该令牌复制回来并发出一个GET请求,添加一个access_token头,其中包含一个令牌字符串作为值。在 Postman 中,headers 部分可用,我们可以在其中添加头和键值对。请参阅以下屏幕截图:

作为 API 响应的一部分,它正确地返回时间。我们还可以看到这是哪个用户的 JWT 令牌。这确认了 RESTAPI 的授权部分。我们不必在每个 API 处理程序中都使用令牌验证逻辑,而可以将其作为中间件,并使其适用于所有处理程序。请参阅第 3 章使用中间件和 RPC,并修改前面的程序,使其具有验证 JWT 令牌的中间件。

基于令牌的认证通常不提供注销 API 或用于删除基于会话的认证中提供的令牌的 API。只要 JWT 未过期,服务器就会将授权资源提供给客户端。一旦到期,客户端需要刷新令牌,也就是说,向服务器请求一个新令牌。

总结

在本章中,我们介绍了认证的过程。我们看到了认证通常是如何工作的。认证可以有两种类型:基于会话的认证或基于令牌的认证。基于会话的认证也称为简单认证,当客户端成功登录时,将创建一个会话。该会话被保存回客户端,并为每个请求提供。这里有两种可能的情况。在第一种情况下,会话将保存在服务器的程序内存中。当应用程序重新启动时,此类会话将被清除。第二种情况是将会话 cookie 保存在 Redis 中。Redis 是一个内存数据库,可以作为任何 web 应用程序的缓存。Redis 支持存储一些数据类型,如字符串、列表、哈希等。我们研究了一个名为redistore的包,它取代了用于持久化会话 cookie 的内置会话包

接下来,我们了解了 JWT。JWT 是一个令牌字符串,它是执行几个步骤的输出。首先,创建标头、有效负载和签名。签名可以通过结合头和有效载荷与base64URL编码并应用加密算法(如 HMAC)来获得。在基于令牌的认证中,客户端需要 JWT 令牌来访问服务器资源。因此,最初,它请求服务器提供访问令牌(JWT 令牌)。一旦客户端获得这个令牌,下次它使用 HTTP 头中的 JWT 令牌进行 API 调用时,服务器将返回响应。

我们介绍了 OAuth2.0,一个认证框架。在 OAuth2 中,客户端首先向资源所有者请求授权。一旦获得授权,它就会从认证服务器请求访问令牌。认证服务器提供访问令牌,客户端可以使用该令牌请求 API。我们用 JWT 实现了 OAuth2 的第二步。

我们使用一个名为 Postman 的工具测试了所有 API。Postman 是一个很好的工具,可以帮助我们在任何机器上快速测试 API。CURL 仅限于 Linux 和 macOS X。Postman 是 Windows 的明智选择,因为它具有 CURL 的所有功能。

从第一章开始,我们学习了如何创建 HTTP 路由、中间件和处理程序。然后,我们将应用程序与数据库链接以存储资源数据。在基础知识之后,我们探讨了性能优化方面,如微服务和 RPC。最后,我们了解了如何部署 web 服务,并使用认证保护它们。****************************