JWT学习
基本概念
jwt: JSON Web Token, 原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,
比如生成这样的结构:
{
"userName": "test",
"role": "Admin",
"expire": 1584271948
}
之后,当用户与服务器通信时,客户在每次请求时都要带上这个JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名(下面会说)。
这样的话, 服务器就不用保存会话数据了,即服务器变为无状态,使其更容易扩展。
当然为了保密, 不会明文传输这个JSON, 而是将其打包到token中, 用户其实每次还给服务端的是这个token.
token是什么样子呢?
三部分: X.Y.Z (其中X, Y, Z分别代表三个字符串), 比如我临时生成的一个:
其中:
X: JWT头(Base64URL编码)
Y: 实际数据(Base64URL编码)
Z: 签名
比如对于下面的token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0.8XbiPJ212JWUwUBmhtrBWbmVdB-jQdayyO29cAUd5qk
JWT头
$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d | jq
{
"alg": "HS256",
"typ": "JWT"
}
解析头部, 可以得到上述结果. alg表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ表示token的类型,JWT令牌统一写为JWT。
数据部分
$ echo 'eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0' | base64 -d | jq
base64: invalid input
{
"aud": "mail.test.com",
"exp": 1584271810,
"iat": 1584271750,
"iss": "pssport.test.com",
"nbf": 1584271750,
"sub": "aaa@qq.com"
}
数据部分可以任意存放自己需要的值, 可以看到也是base64编码的, 所以像一些敏感信息就不能放在这里了, 上述只是举例.
注: 上面的"base64: invalid input"是因为最后少一个=
,
用linux的base64命令解析会有警告, 不影响.
签名
签名部分就是个普通的字符串, 不是base64编码的.
计算方法(以HMACSHA256为例子):
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),
secret)
JWT头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如test.com/?token=xxx)。 Base64中用的三个字符是+
,/
和=
,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:=
去掉,+
用-
替换,/
用_
替换.
我们可以来验证一下, 上面的token用的**是"s3cret".
在https://1024tools.com/hmac这个页面中, 输入图中参数,
其中消息是"X.Y",
看最后一行的结果"结果B:(HMAC计算返回原始二进制数据后进行Base64编码)":
8XbiPJ212JWUwUBmhtrBWbmVdB+jQdayyO29cAUd5qk=
按Base64URL的规则, 将计算结果的+
改为-
, =
去掉, 就和上述的Z部分一样了.
实践
package main
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
var (
secret = []byte("s3cret")
)
func main() {
token, err := CreateToken(secret)
if err != nil {
panic(err)
}
fmt.Println(token)
payload, err := ParseToken(token)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", payload)
}
// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func CreateToken(secret []byte) (token string, err error) {
// 创建token对象, 指定了签名算法和请求参数
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
Audience: "mail.test.com", // 接收方
ExpiresAt: time.Now().Unix() + 60, // 过期时间
IssuedAt: time.Now().Unix(), // 签发时间
Issuer: "pssport.test.com", // 签发者
NotBefore: time.Now().Unix(), // 在此时间之前不可用
Subject: "aaa@qq.com", // 面向的用户
})
// 签名 并 得到 使用**签名后的token
if token, err = jwtToken.SignedString(secret); err != nil {
return
}
return
}
// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func ParseToken(tokenString string) (payload map[string]interface{}, err error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
payload = claims
} else {
err = fmt.Errorf("unexpected claims")
}
return
}
上面的例子演示了如何创建token, 及验证token, 解析出实际数据.
JWT的一般用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header字段中。
参考
下一篇: 那些让你相见恨晚的Photoshop技巧