JWT是什么?
JWT (JSON Web Token)是一个开放标准(RFC 7519),指基于JSON的、用于在网络上声明某种主张的令牌(token),以保证各方之间安全的传输信息。
JWT通过将用户信息加密到token中,服务端不需要保存任何用户信息。服务端只需要通过保存的密钥来验证token正确性,如果正确即通过验证。
JWT的组成
JWS实际上就是一个字符串,由三部分组成,头部(Header)、载荷(Payload)、签名(Signature),并以.
进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码。
1. 头部(Header)
每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg
,同时还有一个typ
的字段,默认JWT
即可。以下示例中算法为HS256。
{
"alg": "HS256",
"typ": "JWT"
}
因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. 载荷(Payload)
载荷即消息体,这里会存放实际的内容,也就是Token的数据声明(Claim)。这一段有一些是标准字段,当然也可以根据自己需要添加自己需要的字段。标准字段如下:
iss
: Token签发者。格式是区分大小写的字符串或者uri,用于唯一标识签发token的一方。sub
: Token的主体,即它的所有人。格式是区分大小写的字符串或者uri。aud
: 接收Token的一方。格式为区分大小写的字符串或uri,或者这两种的数组。exp
: Token的过期时间,格式为时间戳。nbf
: 指定Token在nbf时间之前不能使用,即token开始生效的时间,格式为时间戳。iat
: Token的签发时间,格式为时间戳。jti
: 指此Token的唯一标识符字符串。主要用于实现唯一性保证,防止重放。
下面是一个示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同样进行Base64编码后,字符串如下:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3. 签名(Signature)
签名是对头部和载荷内容进行签名,一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致。
签名的过程:
- 对header的json数据进行Base64URL编码,得到一个字符串str1
- 对payload的json数据进行Base64URL编码,得到一个字符串str2
- 使用
.
对以上两个字符串进行拼接,得到字符串str3 - 使用header中声明的算法,以及服务端的密钥,对拼接字符串进行加密,生成签名
如果用伪代码表示就是(以HS256为例):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
将三组字符串,以.
相连,就得到了一个完整的token,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
如何使用JWT?
在之前的传统的方法时在服务端存储一个session,并给客户端返回一个cookie。而如果是使用jwt来做身份鉴定的话,当用户登录成功,会给用户一个token,前端只需要在本地保存该token即可(通常使用localStorage,也可以使用cookie)。
当用户需要访问一个受保护的资源时,需要再Header中使用Bearer模式的Authorization头。其内容看起来是下面这样:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OFHM3R8PSyHDT_vuzRF5fYkYWdhExM_9pE81kG05qAk
JWT的优缺点
优点:
- json具有通用性,所以可以跨语言。
- 组成简单,字节占用小,便于传输
- 服务端无需保存会话信息,很容易进行水平扩展
- 一处生成,多处使用,可以在分布式系统中,解决单点登录问题
- 可防护CSRF攻击
缺点:
- payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
- 需要保护好加密密钥,一旦泄露后果不堪设想
- 为避免token被劫持,最好使用https协议
- 针对已经办法的令牌,无法作废,不容易应对数据过期的问题。
Golang实现
在https://jwt.io/为我们推荐了几个golang的Package,我们选用其中的github.com/lestrrat-go/jwx/v2
。
安装:
go get -u github.com/lestrrat-go/jwx/v2@latest
代码示例:
package main
import (
"fmt"
"time"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
)
type Conf struct {
Method string // 加密算法
Key string // 加密key
Issuer string // 签发者
Expire int64 // 签名有效期
}
var conf = Conf{
Method: "HS256",
Key: "sahjdjsgaudsiudhuywge",
Issuer: "testIssuer",
Expire: 100,
}
// GetJWT 获取json web token
func GetJWT(data map[string]interface{}) (token string, err error) {
now := time.Now()
b, err := jwt.NewBuilder().
Issuer(conf.Issuer).
IssuedAt(now).
Expiration(now.Add(-time.Duration(conf.Expire) * time.Minute)).
NotBefore(now).Build()
if err != nil {
return
}
tokenBytes, err := jwt.Sign(b, jwt.WithKey(jwa.HS256, []byte(conf.Key)))
if err != nil {
return
}
token = string(tokenBytes)
return
}
// VerifyJWT 验证json web token
func VerifyJWT(token string) (ret bool, err error) {
_, err = jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(conf.Key)))
if err != nil {
return
}
return true, nil
}
func main() {
token, err := GetJWT(map[string]interface{}{})
fmt.Println(token, err)
ret, err := VerifyJWT(token)
fmt.Println(ret, err)
}