参考jwt-go
本文结合JSON Web Token 入门教程了解Golang的jwt-go包的具体实现
JWT 的数据结构
它是一个很长的字符串,中间用点(.)分隔成三个部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9.FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI
三个部分依次如下。
- Header(头部)
- Payload(负载)
- Signature(签名)
在jwt-go包创建的token串写成一行
Header.Claims.Signature
创建token
示例代码
package main
import (
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
type CustomClaimsExample struct {
UserId uint
jwt.StandardClaims
}
const key = "abcd1234" // 密钥
func main() {
token, err := createToken()
if err != nil {
fmt.Println("create token err ", err)
return
}
fmt.Println("token: ", token)
}
func createToken() (tokenStr string, err error) {
claims := &CustomClaimsExample{
UserId: 1,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Unix() + 604800,
Issuer: "AntFoot",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err = token.SignedString([]byte(key))
return
}
运行结果如下
# go run main.go
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9.FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI
创建token的过程,总共包含三部分
定义数据对象
服务器创建一个claims数据对象
claims := &CustomClaimsExample{
UserId: 1,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Unix() + 604800,
Issuer: "AntFoot",
},
}
包括两部分数据
- 自定义数据。这里定义的自定义数据只有一个
UserId: 1一般是服务器经过用户认证之后,比如账号密码验证正确,获取到的此用户的信息,需要传给客户端的数据,此处演示时UserId,还可以是Username等非敏感数据,像Password不可以,因为这个对象只是经过转换json对象,然后经过Base64URL编码转成字符串后,在和header串以及签名串拼接后,返回给客户端 - 通用签名数据。
StandardClaims是声明签名数据的7个官方字段,选用
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
- Audience: aud (audience) 受众
- ExpiresAt: exp (expiration time):过期时间
- Id: jti (JWT ID):编号
- IssuedAt: iat (Issued At):签发时间
- Issuer: iss (issuer):签发人
- NotBefore: nbf (Not Before):生效时间
- Subject: sub (subject):主题
获取jwt.Token结构体
JWT令牌,根据当前在创建令牌还是在解析/验证令牌,将会使用不同的字段
type Token struct {
Raw string // 解析时使用
Method SigningMethod // 定义当前创建或者解析使用的加密算法
Header map[string]interface{} // token串的第一部分
Claims Claims // token串的第二部分,也就是数据部分
Signature string // token串的第三部分,通过Method的加密算法,对Header和Claims部分进行加密,获取的签名串
Valid bool // 解析验证token串时,表示结果是否有效
}
获取用于创建令牌的Token结构体指针
func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: claims,
Method: method,
}
}
赋值的三个属性字段
Header是描述JWT的元数据,在后续加密过程中,转为一个json对象Claims是传递的数据,同样在后续加密过程中,转为一个json对象Method是选择加密算法jwt.SigningMethodHS256
创建token串
返回完整的签名令牌token
func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string
var err error
if sstr, err = t.SigningString(); err != nil {
return "", err
}
if sig, err = t.Method.Sign(sstr, key); err != nil {
return "", err
}
return strings.Join([]string{sstr, sig}, "."), nil
}
先对Token结构体的Header部分和Claims数据部分处理为最终token串Header.Payload.Signature的前两部分数据格式
func (t *Token) SigningString() (string, error) {
var err error
parts := make([]string, 2)
for i, _ := range parts {
var jsonValue []byte
if i == 0 {
if jsonValue, err = json.Marshal(t.Header); err != nil {
return "", err
}
} else {
if jsonValue, err = json.Marshal(t.Claims); err != nil {
return "", err
}
}
parts[i] = EncodeSegment(jsonValue)
}
return strings.Join(parts, "."), nil
}
定义容量为2的parts变量,然后循环这个变量,执行两次
- 第一次,
i == 0,将Token.Header转为一个json对象,赋值给jsonValue变量,然后通过EncodeSegment函数,使用Base64URL编码转成字符串,最后赋值给parts[0],即parts[0] = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - 第二次,将
Token.Claims转为一个json对象,赋值给jsonValue变量,然后通过EncodeSegment函数,使用Base64URL编码转成字符串,最后赋值给parts[1],即parts[1] = eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9
然后,通过strings.Join(parts, ".")将切片变量parts转为.分割的字符串返回,赋值给即sstr字段
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9
接下来,方法t.Method.Sign(sstr, key) 使用密钥key,对Header和Claims字符串,进行加密,返回一个防止数据篡改的签名,赋值给sig字段
FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI
最后strings.Join([]string{sstr, sig}, ".") 是把三部分拼成一个字符串,每个部分之间用"点"(.)分隔,返回给用户。
也就是打印的结果
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9.FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI
服务端通过jwt-go 解析token
示例代码
package main
import (
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
)
type CustomClaimsExample struct {
UserId uint
jwt.StandardClaims
}
const key = "abcd1234" // 密钥
func main() {
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9.FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI"
res, err := parseToken(token)
if err != nil {
fmt.Println("parse token err ", err)
return
}
fmt.Printf("parse token res %T, %+v \n", res, res)
}
func parseToken(tokenStr string) (claims *CustomClaimsExample, err error) {
token, err := jwt.ParseWithClaims(tokenStr, &CustomClaimsExample{}, func(token *jwt.Token) (i interface{}, e error) {
return []byte(key), nil
})
if err != nil {
return
}
if token == nil {
err = errors.New("token 为空")
return
}
if claims, ok := token.Claims.(*CustomClaimsExample); ok && token.Valid {
return claims, nil
}
err = errors.New("解析失败")
return
}
运行结果
# go run main.go
parse token res *main.CustomClaimsExample, &{UserId:1 StandardClaims:{Audience: ExpiresAt:1632470191 Id: IssuedAt:0 Issuer:AntFoot NotBefore:0 Subject:}}
jwt.ParseWithClaims解析token,实际是调用了Parser结构体绑定的ParseWithClaims方法
type Parser struct {
ValidMethods []string // If populated, only these methods will be considered valid
UseJSONNumber bool // Use JSON Number format in JSON decoder
SkipClaimsValidation bool // Skip claims validation during token parsing
}
首先,解析token串,获取赋值后的jwt.Token结构体指针
这个方法只是解析令牌,但不验证签名。
token, parts, err := p.ParseUnverified(tokenString, claims)
首先判断是否符合三段数据 header.claims.signature
将待验证的token串,赋值给Token结构体 token = &Token{Raw: tokenString}
然后解析header数据串,经Base64URL编码,在将json对象结果转为一个map对象,赋值给token.Header属性字段
同样,解析claims数据串,赋值给token.Claims属性字段
最后,判断解析出来的token.Header字段,这个map中,是否包含指定签名算法的字段alg,如果存在,解析出对应的签名方法,赋值给token.Method属性字段
返回数据
token
&Token{
Raw: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9.FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI",
Method: &SigningMethodHMAC{"HS256", crypto.SHA256},
Header: {
"alg": "HS256",
"typ": "JWT",
},
Claims: &CustomClaimsExample{
UserId: 1,
jwt.StandardClaims,
},
Signature: ""
Valid: false
}
parts
[]string{
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"eyJVc2VySWQiOjEsImV4cCI6MTYzMjQ3MDE5MSwiaXNzIjoiQW50Rm9vdCJ9",
"FlktymNKGVJ57DnQgwIpJMZtPgcWNr0GyM423Y5HZrI"
}
验证claims数据
验证claims数据中基于时间的exp, iat, nbf,如果没有上述任何声明,则仍将其视为有效
func (c StandardClaims) Valid() error {
vErr := new(ValidationError)
now := TimeFunc().Unix() // 当前时间戳
// 通过StandardClaims中设置的ExpiresAt值判断是否过期,如果没有设置,则返回true,没有过期
if c.VerifyExpiresAt(now, false) == false {
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
vErr.Inner = fmt.Errorf("token is expired by %v", delta)
vErr.Errors |= ValidationErrorExpired
}
// 通过StandardClaims中设置的IssuedAt值判断签发时间,如果没有设置,则返回true
if c.VerifyIssuedAt(now, false) == false {
vErr.Inner = fmt.Errorf("Token used before issued")
vErr.Errors |= ValidationErrorIssuedAt
}
// 通过StandardClaims中设置的NotBefore值判断生效时间,如果没有设置,则返回true
if c.VerifyNotBefore(now, false) == false {
vErr.Inner = fmt.Errorf("token is not valid yet")
vErr.Errors |= ValidationErrorNotValidYet
}
if vErr.valid() {
return nil
}
return vErr
}
验证签名
最后调用Token.Method中绑定的加密方法,验证签名是否正确,被篡改
token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key)