go 教程:从零实现 jwt 认证 | go优质外文翻译 | go 技术论坛-380玩彩网官网入口

身份验证使应用程序知道向应用程序发送请求的人是谁。json web令牌(jwt)是一种允许身份验证的方法,而无需在系统本身实际存储任何有关用户的任何信息(与相反 )。

在本文中,我们将演示基于jwt的身份验证的工作原理,以及如何在go中构建示例应用程序以实现该示例。

如果你已经知道jwt的工作原理,并且只想看一下实现,则可以 ,或者查看源代码

jwt格式

假设我们有一个名为的用户user1,他们尝试登录到应用程序或网站。一旦成功,他们将收到一个看起来像这样的令牌:

eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vybmftzsi6invzzxixiiwizxhwijoxntq3otc0mdgyfq.2ye5_w1z3zpd4dsgdrp3s98zipcnqqmshrb9vioox54

这是一个jwt,由三部分组成(以分隔.):

  1. 第一部分是标题header(eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9)。标头指定信息,例如用于生成签名的算法(第三部分)。这部分是标准的,并且对于使用相同算法的任何jwt都是相同的。
  2. 第二部分是有效负载payload (eyj1c2vybmftzsi6invzzxixiiwizxhwijoxntq3otc0mdgyfq),其中包含特定于应用程序的信息(在我们的示例中,这是用户名),以及有关令牌的到期和有效性的信息。
  3. 第三部分是签名(2ye5_w1z3zpd4dsgdrp3s98zipcnqqmshrb9vioox54)。它是通过组合和散列前两个部分以及一个秘密密钥来生成的。

现在有趣的是,标题header和有效负载payload未加密。它们只是base64编码的。这意味着任何人都可以通过解码来查看其内容。

例如,我们可以使用此 对标题或有效负载进行解码。

它将显示为以下内容:

{ "alg": "hs256", "typ": "jwt" }

如果您使用的是linux或mac os,也可以在终端上执行以下语句:

echo eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9 | base64 -d

同样,有效负载的内容为:

{ "username": "user1", "exp": 1547974082 }

jwt签名如何工作

因此,如果任何人都可以读写jwt的标头和签名,那么实际上如何保证jwt是安全的?答案在于如何生成最后一部分(签名)。

假设你的应用程序想要向成功登录的用户user1签发jwt。

使标头和有效负载非常简单:标头或多或少是固定的,有效负载json对象是通过设置用户id和有效时间(以unix毫秒为单位)来形成的。

发行令牌的应用程序还拥有一个密钥,该密钥是一个私有值,并且仅对应用程序本身是已知的。然后将标头和有效负载的base64表示形式与密钥组合,然后通过哈希算法计算签名值(在本例中为hs256,如标头中所述)

如何实现算法的细节超出了本文的讨论范围,但是要注意的重要一点是,这是一种hash方法,这意味着我们无法破解算法并获得进行签名的密钥,因此我们秘密密钥仍然是私有的。

验证jwt

为了验证传入的jwt,将使用传入的jwt的标头和有效负载以及密钥再次生成签名。如果签名与jwt上的签名匹配,则认为jwt有效。

现在,让我们假设你是一个试图发行假令牌的黑客。你可以轻松地生成标头和有效负载,但是在不知道密钥的情况下,无法生成有效的签名。如果你尝试篡改有效jwt的有效负载payload,则签名将不再匹配。

这样,jwt可以以一种安全的方式授权用户,而无需在应用程序服务器上实际存储任何信息(除了密钥)。

go的实现

现在,我们已经了解了基于jwt的身份验证的工作原理,让我们使用go来实现它。

创建http服务器

首先让我们初始化需要使用的http服务器路由:

package main
import (
    "log"
    "net/http"
)
func main() {
    // "signin"和"welcome"方法是我们将要实现的处理程序
    http.handlefunc("/signin", signin)
    http.handlefunc("/welcome", welcome)
    http.handlefunc("/refresh", refresh)
    // 在8000端口启动服务
    log.fatal(http.listenandserve(":8000", nil))
}

现在,我们可以定义signinwelcome路由。

处理用户登录

/signin路由将获取用户凭据并登录。为简化起见,我们在代码中将用户信息存储在map:

var users = map[string]string{
    "user1": "password1",
    "user2": "password2",
}

因此,目前,我们的应用程序中只有两个有效用户: user1user2。接下来,我们可以编写signinhttp处理程序。对于此示例,我们使用 库来帮助我们创建和验证jwt令牌。

import (
  //...
  // 导入jwt-go库
    "github.com/dgrijalva/jwt-go"
    //...
)
// 创建一个jwt使用的密钥
var jwtkey = []byte("my_secret_key")
var users = map[string]string{
    "user1": "password1",
    "user2": "password2",
}
// 创建一个结构以从请求正文中读取用户名和密码
type credentials struct {
    password string `json:"password"`
    username string `json:"username"`
}
// 创建将被编码为jwt的结构。
// 我们将jwt.standardclaims作为嵌入式类型,以提供到期时间等字段。
type claims struct {
    username string `json:"username"`
    jwt.standardclaims
}
// 创建signin处理函数。
func signin(w http.responsewriter, r *http.request) {
    var creds credentials
    // 获取json正文并解码为凭据
    err := json.newdecoder(r.body).decode(&creds)
    if err != nil {
        // 如果主体结构错误,则返回http错误
        w.writeheader(http.statusbadrequest)
        return
    }
    // 从我们的map中获取用户的密码
    expectedpassword, ok := users[creds.username]
    // 如果设置的用户密码与我们收到的密码相同,那么我们可以继续。
    // 如果不是,则返回“未经授权”状态。
    if !ok || expectedpassword != creds.password {
        w.writeheader(http.statusunauthorized)
        return
    }
    // 在这里声明令牌的到期时间,我们将其保留为5分钟
    expirationtime := time.now().add(5 * time.minute)
    // 创建jwt声明,其中包括用户名和有效时间
    claims := &claims{
        username: creds.username,
        standardclaims: jwt.standardclaims{
            // in jwt, the expiry time is expressed as unix milliseconds
            expiresat: expirationtime.unix(),
        },
    }
    // 使用用于签名的算法和令牌
    token := jwt.newwithclaims(jwt.signingmethodhs256, claims)
    // 创建jwt字符串
    tokenstring, err := token.signedstring(jwtkey)
    if err != nil {
        // 如果创建jwt时出错,则返回内部服务器错误
        w.writeheader(http.statusinternalservererror)
        return
    }
    // 最后,我们将客户端cookie token设置为刚刚生成的jwt
    // 我们还设置了与令牌本身相同的cookie到期时间
    http.setcookie(w, &http.cookie{
        name:    "token",
        value:   tokenstring,
        expires: expirationtime,
    })
}

如果用户使用正确的凭据登录,则此处理程序将使用jwt值在客户端设置cookie。一旦在客户端上设置了cookie,此后它将与每个请求一起发送。现在,我们可以编写welcome方法来处理用户特定的信息。

处理认证后的路由

现在,所有已登录的客户端都使用cookie存储用户信息,我们可以将其用于:

  • 验证后续用户请求
  • 获取有关发出请求的用户的信息

让我们编写welcome处理方法来做到这一点:

func welcome(w http.responsewriter, r *http.request) {
    // 我们可以从每个请求的cookie中获取会话令牌
    c, err := r.cookie("token")
    if err != nil {
        if err == http.errnocookie {
            // 如果未设置cookie,则返回未授权状态
            w.writeheader(http.statusunauthorized)
            return
        }
        // 对于其他类型的错误,返回错误的请求状态。
        w.writeheader(http.statusbadrequest)
        return
    }
    // 从cookie获取jwt字符串
    tknstr := c.value
    // 初始化`claims`实例
    claims := &claims{}
    // 解析jwt字符串并将结果存储在`claims`中。
    // 请注意,我们也在此方法中传递了密钥。 
    // 如果令牌无效(如果令牌已根据我们设置的登录到期时间过期)或者签名不匹配,此方法会返回错误.
    tkn, err := jwt.parsewithclaims(tknstr, claims, func(token *jwt.token) (interface{}, error) {
        return jwtkey, nil
    })
    if err != nil {
        if err == jwt.errsignatureinvalid {
            w.writeheader(http.statusunauthorized)
            return
        }
        w.writeheader(http.statusbadrequest)
        return
    }
    if !tkn.valid {
        w.writeheader(http.statusunauthorized)
        return
    }
    // 最后,将欢迎消息以及令牌中的用户名返回给用户
    w.write([]byte(fmt.sprintf("welcome %s!", claims.username)))
}

续签令牌

在此示例中,我们将有效期设置为五分钟。如果令牌过期,我们不希望用户每五分钟登录一次。为了解决这个问题,我们将创建另一个/refresh路由,该路由使用先前的令牌仍然有效,并返回更新到期时间的新令牌。

为了最大程度地减少对jwt的滥用,通常将到期时间保持在几分钟左右。通常,客户端应用程序将在后台刷新令牌。

func refresh(w http.responsewriter, r *http.request) {
    // (begin) 此处的代码与`welcome`路由的第一部分相同
    c, err := r.cookie("token")
    if err != nil {
        if err == http.errnocookie {
            w.writeheader(http.statusunauthorized)
            return
        }
        w.writeheader(http.statusbadrequest)
        return
    }
    tknstr := c.value
    claims := &claims{}
    tkn, err := jwt.parsewithclaims(tknstr, claims, func(token *jwt.token) (interface{}, error) {
        return jwtkey, nil
    })
    if err != nil {
        if err == jwt.errsignatureinvalid {
            w.writeheader(http.statusunauthorized)
            return
        }
        w.writeheader(http.statusbadrequest)
        return
    }
    if !tkn.valid {
        w.writeheader(http.statusunauthorized)
        return
    }
    // (end)  此处的代码与`welcome`路由的第一部分相同
    // 我们确保在足够的时间之前不会发行新令牌。
    // 在这种情况下,仅当旧令牌在30秒到期时才发行新令牌。  
    // 否则,返回错误的请求状态。
    if time.unix(claims.expiresat, 0).sub(time.now()) > 30*time.second {
        w.writeheader(http.statusbadrequest)
        return
    }
    // 现在,为当前用户创建一个新令牌,并延长其到期时间
    expirationtime := time.now().add(5 * time.minute)
    claims.expiresat = expirationtime.unix()
    token := jwt.newwithclaims(jwt.signingmethodhs256, claims)
    tokenstring, err := token.signedstring(jwtkey)
    if err != nil {
        w.writeheader(http.statusinternalservererror)
        return
    }
    // 查看用户新的`token` cookie
    http.setcookie(w, &http.cookie{
        name:    "token",
        value:   tokenstring,
        expires: expirationtime,
    })
}

运行我的程序

要运行此应用程序,请编译并运行go二进制文件:

go build
./jwt-go-example

现在,使用任何支持cookie的http客户端(例如或您的web浏览器),使用适当的凭据发出登录请求:

post http://localhost:8000/signin
{"username":"user1","password":"password1"}

现在,您可以尝试点击来自同一客户的/welcome路径显示欢迎消息:

get http://localhost:8000/welcome

访问刷新路径,然后检查客户端cookie以及查看新生成的cookietoken

post http://localhost:8000/refresh

在此处找到此示例的 。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 cc 协议,如果我们的工作有侵犯到您的权益,请及时联系380玩彩网官网入口。

原文地址:

译文地址:https://learnku.com/go/t/52399

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 5

make 一下

4年前

没有refresh_token,而不是续签?

3年前

mark一下下

3年前

mark一下

1年前

学到了,mark一下

1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
网站地图