Golang jwt 认证

1,423 阅读6分钟

参考JSON Web Token 入门教程

参考jwt-go

本文结合JSON Web Token 入门教程了解Golangjwt-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",
    },
}

包括两部分数据

  1. 自定义数据。这里定义的自定义数据只有一个UserId: 1 一般是服务器经过用户认证之后,比如账号密码验证正确,获取到的此用户的信息,需要传给客户端的数据,此处演示时UserId,还可以是Username等非敏感数据,像Password不可以,因为这个对象只是经过转换json对象,然后经过Base64URL编码转成字符串后,在和header串以及签名串拼接后,返回给客户端
  2. 通用签名数据。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,
	}
}

赋值的三个属性字段

  1. Header是描述JWT的元数据,在后续加密过程中,转为一个json对象
  2. Claims 是传递的数据,同样在后续加密过程中,转为一个json对象
  3. 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,对HeaderClaims字符串,进行加密,返回一个防止数据篡改的签名,赋值给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)