通过Golang深入理解JWT[JSON Web Token]


原文链接: 通过Golang深入理解JWT[JSON Web Token]

JWT是什么?

JWT是JSON Web Token的缩写,即JSON Web令牌。
JWT规范中对其所作的描述是:

JSON Web令牌(JWT)是一种紧凑的、URL安全的方式,用来表示要在双方之间传递的“声明”。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使得声明能够被:数字签名、或利用消息认证码(MAC)保护完整性、加密。

JWT的声明(Claims)就是一小段信息,用“键-值”对表示。

想要详细了解JSON Web签名(JWS)JSON Web加密(JWE),可以自行去IETF的网站查阅规范,下文中我会简单的介绍它们。

JWT的构成

JWT由三部分组成:

Header:头部,   即JOSE Header
Claims:声明,   即JWS Paylaod
Signature:签名,即JWS Signature

JWT由这三部分组成,每一部分都是使用base64url编码的,并使用句点(.)连接起来。这里使用base64url编码而不是普通的base64,是因为base64编码会产生+和/,这两个字符在URL中是有特殊意义的,会导致JWT不是URL安全的。
JWT.io首页的一个例子介绍JWT的组成。再用Golang通过这些JSON对象生成JWT,最后用jwt-go包比对生成的JWT。

JWT标准并没有规定必须清除JSON结构中开头结尾的空白符和换行,但是为了消除歧义,一般在使用JSON对象时不用换行,并去掉多余的空白符,这会在我们的代码中有所体现。

为了方便查看,下面展示代码时使用的都是格式化后的JSON对象。

1. 头部(JOSE Header)

JSOE是JSON Object Signing and Encryption,即JSON对象签名与加密的缩写。

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

示例中给出了两个声明:

typ: (Type)类型。在JOSE Header中这是个可选参数,但这里我们需要指明类型是JWT。
alg: (Algorithm)算法,必须是JWS支持的算法,算法列表可以在 [JSON Web算法 JWA](https://tools.ietf.org/html/rfc7518) 这里指定算法为HS256

例子中只列举了两个声明,更多的声明和其具体定义可以到 JSON Web签名(JWS)中查看。

Golang代码:

...
header := []byte(`{
  "typ": "JWT",
  "alg": "HS256"
}`)

buffer := new(bytes.Buffer)
//去掉多余的换行和空白符
json.Compact(buffer, header)
//Base64URL编码
jwtHeader := base64.URLEncoding.EncodeToString(buffer.Bytes())
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
fmt.Println(jwtHeader)
...

上述代码片段会输出eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,这就是编码后的JWT头部。

2. 声明(JWT Claims)

在一个声明集当中,一般会有如下注册的声明名字:

sub: (Subject)该JWT的主题
iss: (Issuer)签发者    
aud: (Audience)接收该JWT的一方
iat: (Issued At)签发时间,用Unix时间戳表示
exp: (Expiration Time)过期时间,用Unix时间戳表示
nbf: (Not Before)不要早于这个时间
jti: (JWT ID)用于标识JWT的唯一ID

上面的声明都是可选的,但是一般都达成共识,
注册的声明是在IANA中注册的,
公开的声明要保证不引起命名冲突

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

例子中给的是一个注册的声明(sub),和两个私有的声明(name和admin)。
注册的、公开的、私有的

私有的声明可以使用

Golang代码:

...
claims := []byte(`{
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}`)

buffer := new(bytes.Buffer)
json.Compact(buffer, claims)
jwtClaims := base64.URLEncoding.EncodeToString(buffer.Bytes())
fmt.Println(jwtClaims)
...

上述代码片段会输出eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9,这就是编码后的JWT声明。

3. 签名(Signature)

按照头部中指定的,我们要使用HS256算法对上面的编码后的字符串进行签名。
头部和声明用.号连接起来:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

我们要做的就是对这个字符串进行签名。

...
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
s := strings.Join([]string{jwtHeader, jwtClaims}, ".")
//HS256算法,key是"secret"
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte(s))
expectedMAC := mac.Sum(nil)
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
signature := strings.TrimRight(base64.URLEncoding.EncodeToString(expectedMAC), "=")
fmt.Println(signature)
...

上述代码输出TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ,这就是这个JWT的签名。

将头部、声明、签名用.号连在一起就得到了我们要的JWT。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

验证

//定义
type MyCustomClaims struct {
    Sub   string `json:"sub"`
    Name  string `json:"name"`
    Admin bool   `json:"admin"`
}
//实现Claims接口
func (m MyCustomClaims) Valid() error {
    return nil
}

mySigningKey := []byte("secret")

claims2 := MyCustomClaims{
    "1234567890",
    "John Doe",
    true,
}


token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims2)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v\n", ss, err)
if ss == s {
    fmt.Println("OK")
}
...

// Encode JWT specific base64url encoding with padding stripped
func EncodeSegment(seg []byte) string {
    return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}

// Decode JWT specific base64url encoding with padding stripped
func DecodeSegment(seg string) ([]byte, error) {
    if l := len(seg) % 4; l > 0 {
        seg += strings.Repeat("=", 4-l)
    }
    return base64.URLEncoding.DecodeString(seg)
}

不安全的JWT

签名为空的JWT
创建JWT

按一下步骤创建:

对UTF-8的八进制序列进行Base64url编码
一些可以应用JWT的案例

注意:下面的例子设计并不完善,甚至存在漏洞。这里仅仅是展示JWT的用途。不要将例子直接用于生产环境。
验证用户
签发JWT

1.客户端发送带有用户名、密码的表单到服务器;
2.服务器验证用户名密码后,将user_id作为JWT Claims中的一个声明,生成JWT;
3.将签发的JWT作为cookies的内容发送给用户。

这里要注意,JWT作为cookies的一部分,本质上还是cookies,所以还是要遵循一般的安全原则,防止XSS等攻击手段。
验证请求

1.客户端发送带有JWT的请求到服务器;
2.服务器从JWT中提取信息;
3.验证JWT是否合法(签名是否正确、令牌是否过期、请求时间在nbf之前还是之后、签发人是否被接受、服务器是否是真正的接受者等);
4.从声明中取出user_id
和session的区别

session需要在服务器中存储标记用户的信息,比如session_id,而JWT则需要。

JWT在服务器端需要一定量的计算,而session方式一般不需要。

在分布式系统中,使用Session的方式,需要在多台服务器之间session id,增加了服务器的内存和IO压力。而JWT方式则免去了同步的麻烦。因为用户的状态已经存储在客户端中了,虽然增加了一些计算开销,但是与IO开销比起来,还是要好很多的。

单点登录

Set-Cookie: jwt=header.claims.signature; HttpOnly; max-age=980000; domain=.yourdomain.com

我们将域名设置为顶级域名(域名前要加.),这样yourdomain.com和*.yourdomain.com都能接收这个cookies了。
免登陆退订订阅邮件功能

我们的邮箱中经常会收到一些订阅邮件,有一些
一些有用的链接

JWT.io

`