未完待续
JWT是什么?
JWT是JSON Web Token的缩写,即JSON Web令牌。
<a href="https://tools.ietf.org/html/rfc7519" target="_blank">JWT规范</a>中对其所作的描述是:
JSON Web令牌(JWT)是一种紧凑的、URL安全的方式,用来表示要在双方之间传递的“声明”。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使得声明能够被:数字签名、或利用消息认证码(MAC)保护完整性、加密。
JWT的声明(Claims)就是一小段信息,用“键-值”对表示。
想要详细了解<a href="https://tools.ietf.org/html/rfc7515" target="_blank">JSON Web签名(JWS)</a>和<a href="https://tools.ietf.org/html/rfc7516" target="_blank">JSON Web加密(JWE)</a>,可以自行去IETF的网站查阅规范,下文中我会简单的介绍它们。
JWT的构成
JWT由三部分组成:
-
Header
:头部,即JOSE Header
-
Claims
:声明,即JWS Paylaod
-
Signature
:签名,即JWS Signature
JWT由这三部分组成,每一部分都是使用base64url编码的,并使用句点(.
)连接起来。这里使用base64url编码而不是普通的base64,是因为base64编码会产生+
和/
,这两个字符在URL中是有特殊意义的,会导致JWT不是URL安全的。
下面以<a href="http://jwt.io" target="_blank">JWT.io</a>首页的一个例子介绍JWT的组成。再用Golang通过这些JSON对象生成JWT,最后用<a href="http://github.com/dgrijalva/jwt-go" target="_blank">jwt-go</a>包比对生成的JWT。
JWT标准并没有规定必须清除JSON结构中开头结尾的空白符和换行,但是为了消除歧义,一般在使用JSON对象时不用换行,并去掉多余的空白符,这会在我们的代码中有所体现。
为了方便查看,下面展示代码时使用的都是格式化后的JSON对象。
头部(JOSE Header)
JSOE
是JSON Object Signing and Encryption
,即JSON对象签名与加密
的缩写。
{
"typ": "JWT",
"alg": "HS256"
}
示例中给出了两个声明:
-
typ
: (Type)类型。在JOSE Header中这是个可选参数,但这里我们需要指明类型是JWT
。 -
alg
: (Algorithm)算法,必须是JWS支持的算法,算法列表可以在<a href="https://tools.ietf.org/html/rfc7518" target="_blank">JSON Web算法(JWA)</a>。这里指定算法为HS256
例子中只列举了两个声明,更多的声明和其具体定义可以到<a href="https://tools.ietf.org/html/rfc7515" target="_blank">JSON Web签名(JWS)</a>中查看。
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头部。
声明(JWT Claims)
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
例子中给的是一个注册的声明(sub
),和两个私有的声明(name
和admin
)。
注册的、公开的、私有的
在一个声明集当中,一般会有如下注册的声明名字:
-
iss
: (Issuer)签发者 -
iat
: (Issued At)签发时间,用Unix时间戳表示 -
exp
: (Expiration Time)过期时间,用Unix时间戳表示 -
aud
: (Audience)接收该JWT的一方 -
sub
: (Subject)该JWT的主题 -
nbf
: (Not Before)不要早于这个时间 -
jti
: (JWT ID)用于标识JWT的唯一ID
上面的声明都是可选的,但是一般都达成共识,
注册的声明是在IANA中注册的,
公开的声明要保证不引起命名冲突
私有的声明可以使用
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声明。
签名(Signature)
按照头部中指定的,我们要使用HS256
算法对上面的编码后的字符串进行签名。
头部和声明用.
号连接起来:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
我们要做的就是对这个字符串进行签名。
Golang代码:
...
//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了。
免登陆退订订阅邮件功能
我们的邮箱中经常会收到一些订阅邮件,有一些
一些有用的链接
<a href="http://jwt.io" target="_blank">JWT.io</a>
<a href="https://www.sitepoint.com/using-json-web-tokens-node-js/" target="_blank">Using JSON Web Tokens with Node.js</a>