go 教程:从零实现 jwt 认证 | go优质外文翻译 | go 技术论坛-金年会app官方网
身份验证使应用程序知道向应用程序发送请求的人是谁。json web令牌(jwt)是一种允许身份验证的方法,而无需在系统本身实际存储任何有关用户的任何信息(与相反 )。
在本文中,我们将演示基于jwt的身份验证的工作原理,以及如何在go中构建示例应用程序以实现该示例。
如果你已经知道jwt的工作原理,并且只想看一下实现,则可以 ,或者查看源代码
jwt格式
假设我们有一个名为的用户user1
,他们尝试登录到应用程序或网站。一旦成功,他们将收到一个看起来像这样的令牌:
eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vybmftzsi6invzzxixiiwizxhwijoxntq3otc0mdgyfq.2ye5_w1z3zpd4dsgdrp3s98zipcnqqmshrb9vioox54
这是一个jwt,由三部分组成(以分隔.
):
- 第一部分是标题header(
eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9
)。标头指定信息,例如用于生成签名的算法(第三部分)。这部分是标准的,并且对于使用相同算法的任何jwt都是相同的。 - 第二部分是有效负载payload (
eyj1c2vybmftzsi6invzzxixiiwizxhwijoxntq3otc0mdgyfq
),其中包含特定于应用程序的信息(在我们的示例中,这是用户名),以及有关令牌的到期和有效性的信息。 - 第三部分是签名(
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))
}
现在,我们可以定义signin
和welcome
路由。
处理用户登录
/signin
路由将获取用户凭据并登录。为简化起见,我们在代码中将用户信息存储在map:
var users = map[string]string{
"user1": "password1",
"user2": "password2",
}
因此,目前,我们的应用程序中只有两个有效用户: user1
和 user2
。接下来,我们可以编写signin
http处理程序。对于此示例,我们使用 库来帮助我们创建和验证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 协议,如果我们的工作有侵犯到您的权益,请及时联系金年会app官方网。
原文地址:
make 一下
没有refresh_token,而不是续签?
mark一下下
mark一下
学到了,mark一下