使用JWT进行用户鉴权 | 青训营

113 阅读5分钟

简介

JWT(JSON Web Token),即JSON网络令牌,是一种用于安全传递声明的标准化方式,被广泛应用于身份验证和授权场景,它是一种用于在网络间安全传递声明的一种基于JSON的开放标准。

JWT通常用于在各种网络应用中进行身份验证和授权。服务器将用户的认证信息以JWT的形式生成并返回给客户端,客户端在后续的请求中将该JWT作为身份标识发送至服务器进行验证。由于JWT是使用签名进行验证的,因此服务器可以轻松地验证JWT的真实性,并根据其中的声明信息对用户进行授权。

优缺点

JWT具有以下优点:

  1. 无需在服务端存储会话信息,因为JWT中已经包含了所有需要的信息。
  2. 简洁轻量,可以通过网络传输。
  3. 可以轻松跨平台使用,因为JWT基于开放的标准。
  4. 可以对JWT进行签名和加密,提高安全性。

JWT具有以下缺点:

  1. 无法撤销:JWT一旦签发,就无法在有效期内撤销或失效。如果存在安全风险或令牌泄漏,需要等待令牌过期才能解决问题。
  2. 无法修改:JWT一旦签发,其内容是不可修改的。如果需要修改某些信息,比如增加权限等,需要重新签发新的JWT。
  3. 数据量较大:由于JWT包含了签名和一些其他信息,相较于传统的session方案,JWT的数据量较大,会增加网络传输的负荷。
  4. 无法处理会话状态:由于JWT是无状态的,服务器无法在多个请求之间维护会话状态。这在某些应用场景下可能会造成一些问题。
  5. 安全性依赖密钥管理:JWT的安全性依赖于密钥管理的安全性。如果密钥泄漏,可能导致他人伪造合法的JWT。
  6. 不支持刷新机制:JWT本身不支持刷新机制,即一旦令牌失效,就需要重新登录获取新的令牌。这对于一些需要长期授权的场景可能不太方便。

结构

JWT由三部分组成,包括头部(Header)、有效载荷(Payload)和签名(Signature)。头部包含描述JWT的元数据,有效载荷包含被称为声明的各种数据,签名是基于头部和有效载荷计算得到的,用于验证JWT的真实性和完整性。

我们来看一个官方网页的例子,更加直观的了解JWT的结构

image.png

右侧是JWT的三个组成部分。可以看到头部和载荷是JSON格式,其中包含各种我们需要的数据,而签名则是经过HMACSHA256加密的字符串,这里面包括上面的两个部分外加你自己的secret。当然你可以根据需要配置你想包括和签名的内容,不需要一模一样。

实现

在golang中颁发和验证jwt很容易,有现成的包来供我们使用。

安装

在命令行输入go get github.com/dgrijalva/jwt-go来安装jwt-go。 完成后,将它import到我们的项目中。

颁发JWT

我们先定义一个结构体Claims来定义我们需要在载荷中存储哪些字段。

type Claims struct {
	UserId int64
	jwt.StandardClaims
}

这里使用了go-jwt包中提供的StandardClaims为我们写好了常用的字段。这里我们就只多使用一个UserId字段,这样鉴权时就能从载荷中读出用户的id。

然后我们看一下颁发JWT的函数

// 颁发token
func ReleaseToken(userId int64) (tokenStr string, err error) {
	expirationTime := time.Now().Add(7 * 24 * time.Hour)
	claims := &Claims{
		UserId: userId,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(),
			IssuedAt:  time.Now().Unix(),
			Issuer:    "richard",
		}}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenStr, err = token.SignedString([]byte("my-very-long-256-bit-secret"))
	return
}

传入用户的id记录到UserId字段中,获取当前的时间记录到IssuedAt中,加上过期时间(二十四小时)后记录到ExpiresAt中,再将Issuer指定为发行人的标识字符串。之后直接调用封装好的方法NewWithClaims,指定加密算法为jwt.SigningMethodHS256来加密,传入刚刚获得的claims来创建一个token。这个时候还不是最终的JWT字符串,还需要签名才能生成。继续执行token.SignedString,传入你的secret就可以获得到结果了。整体的码量很少,非常简洁美观。你可以将secret存放到例如config这样的包或者环境变量中更加优雅和安全的访问这个核心的密钥。

验证和解析我们的token就更加容易了,下面是实现的代码

// 解析token
func ParseToken(tokenString string) (claims *Claims, ok bool) {
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte("my-very-long-256-bit-secret"), nil
	})
	if err != nil {
		return
	}

	claims, ok = token.Claims.(*Claims)
	return
}

通过ParseWithClaims方法,传入JWT字符串,Claim结构体,和一个名为keyFunc的函数返回secret密钥。当然这里我们只使用了一个密钥,如果你根据不同的头部和载荷信息使用了不同的密钥,可以KeyFunc的参数token中获取到,并且返回对应的密钥。 在Parse完之后如果没有error我们获得到了解析完整的token结构体,可以通过将它的Claims转换成我们定义的Claims,并返回给其他函数使用。

总结

通过了解JWT的结构和实现原理,并且利用jwt-go包实现了简单的JWT颁发和验证,可以在敏感操作中使用JWT来验证操作者的合法性。当然,一般都会作为中间件来使用,减少重复的代码。