JWT生成、解析过程及原生实现

 token是项目开发中常用的一种身份验证机制。本质上,是因为HTTP请求是一种无状态的请求,每一个请求之间是独立的,我们无法直接从请求中判断用户的状态,比如:用户是否登录、用户是否有权限访问该接口等等。因此我们需要额外的信息,帮助我们对用户的状态进行判断。Session、Token这是为了解决这类问题了产生的,但是他们本质上又有一些不同。感兴趣的读者可以直接查阅,后期我也会写相关的博客的。

Token的使用流程:

1. 第一次登录的时候从服务端获取Token

2. 之后将服务端返回的Token,放置到请求头中,一般是Authorization字段。

3. token是有有效期的,如果超过了有效期,就需要重新登陆向服务端再次获取Token。也有类似无感刷新token的技术——长短token。短token的有效期短,长token的有效期长,短token过期了,通过长token向服务端获取token,从而无感刷新token,提高用户的体验(不需要频繁登录了)

什么是JWT:

JWT是token的一种具体实现,相当于是JSON格式的token。以下是官网详解:

JSON Web 令牌(JWT)是一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。每条信息都可以被验证和信任,因为可以对其进行数字签名。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

主要应用:

  • 授权:用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。

  • 信息安全传输:JWT可以签名,因此我们可以验证发送人的身份。此外,由于JWT的签名是使用标头和有效负载计算的,可以验证信息在传递过程中是否发生了篡改。

JWT的结构:

  • JWT 的结构通常由标头有效载荷签名三部分组成,这三部分由点(.)分隔。

  • 标头通常由两部分组成:令牌的类型和正在使用的签名算法,例如 HMAC SHA256 或 RSA。

  • 有效载荷是实际存放传递信息的位置,包含声明和用户自定义信息。其中,声明是关于Token实体的描述,例如:颁发者、过期时间、主题等,声明可以分为三类:已注册声明、公共和私有声明。

  • 签名主要用于验证消息在传递的过程中是否发生篡改,在使用私钥签名的令牌的情况下,还可以验证 JWT 的发件人是否是它所声称的身份。签名并不是对JWT加密,而是保证JWT在传递的过程中不被篡改和验证JWT签发人的身份。

JWT的格式:

xxxxxxxxxxxxxxxxx.yyyyyyyyyyyyy.zzzzzzzzzzz

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

jwt的生成过程:

  1. 标头经过 Base64Url 编码成为JWT的第一个部分。

  2. 对有效负载进行 Base64Url 编码,形成JWT的第二部分。

  3. 获取到标头中指定的签名算法,然后对标头信息、有效负载信息、密钥进行签名。

  4. 最后,我们将这三个部分的字符串使用.进行拼接,形成上方格式的token。

// 如果使用 HMAC SHA256 算法,将按以下方式创建签名:
// 标头和有效载荷部分进行base6Url编码并拼接在一起,生成一个字符串。
//最后使用SHA256生成签名

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

代码实现:

了解了JWT的理论知识之后,我们就可以生成JWT和对JWT的进行验证和判断是否过期。

生成JWT:

type tokenHeader struct {
	Alg string `json:"alg"`
	Typ string `json:"typ"`
}


type tokenPayLoad struct {
	Sub    string `json:"sub"`
	Exp    int64  `json:"exp"`
	UserId int    `json:"user_id"`
}

//生成JWT
func CreateJWT(hashAlg string, secret string, expire int64, userId int) (string, error) {
	//1. 生成JWT的第一部分——标头
	header := tokenHeader{Typ: "JWT", Alg: hashAlg}
	headerMar, _ := json.Marshal(header)
	base64UrlHeader := base64.URLEncoding.EncodeToString(headerMar)

	//2. 生成JWT的第二部分——有效载荷
	payload := tokenPayLoad{Sub: "AccessTok", Exp: time.Now().Unix() + expire, UserId: userId}
	payloadMar, _ := json.Marshal(payload)
	base64UrlPayload := base64.URLEncoding.EncodeToString(payloadMar)
	fmt.Println(payload)

	//3. 生成JWT的第三部分——签名
	base64Signature, err := SignHS256(base64UrlHeader+"."+base64UrlPayload, secret)
	if err != nil {
		log.Fatal("signature process happen error:", err)
		return "", err
	}
	// 4. 进行最终token的拼接,并返回结果
	token := base64UrlHeader + "." + base64UrlPayload + "." + base64Signature
	return token, nil
}

验证和解析JWT:

// 验证Token
func VerifyToken(token string, secret string) (bool, error) {
	// 1. 解析token的各个部分
	firstNodeIndex := strings.Index(token, ".")
	lastNodeIndex := strings.LastIndex(token, ".")
	headerAndPayLoad := token[0:lastNodeIndex]
	base64Signature := token[lastNodeIndex+1:]
	// 2. 进行签名验证,检验token在传递的过程中是否发生了篡改
	ok, err := VerifyHS256(headerAndPayLoad, base64Signature, secret)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, errors.New("token isValid")
	}
	// 3. 检验token是否过期
	payLoad := tokenPayLoad{}
	base64Payload := token[firstNodeIndex+1 : lastNodeIndex]
	decodeString, _ := base64.URLEncoding.DecodeString(base64Payload)
	json.Unmarshal(decodeString, &payLoad)

	isExpire := VerifyExpire(payLoad.Exp)
	if !isExpire {
		return false, errors.New("token is expire")
	}

	return true, nil
}


// VerifyHS256 验证使用 HMAC SHA-256 || 实现:签名比对
func VerifyHS256(data, signature, secret string) (bool, error) {
	expectedSignature, err := SignHS256(data, secret)
	if err != nil {
		fmt.Println("gender signature by head and payload when check sign", err)
		return false, nil
	}

	// 比较签名,注意要处理 Base64 编码的比较问题
	return hmac.Equal([]byte(expectedSignature), []byte(signature)), nil
}

// VerifyExpire 验证token
func VerifyExpire(deadline int64) bool {
	return time.Unix(deadline, 0).After(time.Now())
}

HS256签名算法:

// SignHS256 使用 HMAC SHA-256 算法对给定的数据进行签名,返回编码后的签名
func SignHS256(data, secret string) (string, error) {
	key := []byte(secret)
	h := hmac.New(sha256.New, key)
	_, err := h.Write([]byte(data))
	if err != nil {
		return "", err
	}
	hash := h.Sum(nil)
	return base64.URLEncoding.EncodeToString(hash), nil
}

完整代码:

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"
)

type tokenHeader struct {
	Alg string `json:"alg"`
	Typ string `json:"typ"`
}

type tokenPayLoad struct {
	Sub    string `json:"sub"`
	Exp    int64  `json:"exp"`
	UserId int    `json:"user_id"`
}

func main() {
	hashAlg := "HS256"              //签名时用到的算法
	secretKey := "WuQiong"          //签名是用到的密钥,验证token在传递的过程中是否发生篡改
	tokenValidScope := 12 * 60 * 60 //token的有效时间,以秒为单位,12小时后过期
	userId := 1                     //有效负载中的信息,也可以是其他的,能够标识信息、身份即可

	jwt, _ := CreateJWT(hashAlg, secretKey, int64(tokenValidScope), userId)
	fmt.Println(jwt)

	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBY2Nlc3NUb2siLCJleHAiOjE3Mjg3NDEyNDcsInVzZXJfaWQiOjF9.IU5-zFexc_ktr4cq3ncwPJ-Hm5jvm-9x6ZYlwwKlP4s="
	isValid, err := VerifyToken(token, "WuQiong")
	if err != nil {
		log.Fatal("invalid token  ", err)
	}
	if !isValid {
		fmt.Println("token invalid")
		return
	}
	fmt.Println("token 合法")
}

func CreateJWT(hashAlg string, secret string, expire int64, userId int) (string, error) {
	//1. 生成JWT的第一部分——标头
	header := tokenHeader{Typ: "JWT", Alg: hashAlg}
	headerMar, _ := json.Marshal(header)
	base64UrlHeader := base64.URLEncoding.EncodeToString(headerMar)

	//2. 生成JWT的第二部分——有效载荷
	payload := tokenPayLoad{Sub: "AccessTok", Exp: time.Now().Unix() + expire, UserId: userId}
	payloadMar, _ := json.Marshal(payload)
	base64UrlPayload := base64.URLEncoding.EncodeToString(payloadMar)
	fmt.Println(payload)

	//3. 生成JWT的第三部分——签名
	base64Signature, err := SignHS256(base64UrlHeader+"."+base64UrlPayload, secret)
	if err != nil {
		log.Fatal("signature process happen error:", err)
		return "", err
	}
	// 4. 进行最终token的拼接,并返回结果
	token := base64UrlHeader + "." + base64UrlPayload + "." + base64Signature
	return token, nil
}

func VerifyToken(token string, secret string) (bool, error) {
	// 1. 解析token的各个部分
	firstNodeIndex := strings.Index(token, ".")
	lastNodeIndex := strings.LastIndex(token, ".")
	headerAndPayLoad := token[0:lastNodeIndex]
	base64Signature := token[lastNodeIndex+1:]
	// 2. 进行签名验证,检验token在传递的过程中是否发生了篡改
	ok, err := VerifyHS256(headerAndPayLoad, base64Signature, secret)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, errors.New("token isValid")
	}
	// 3. 检验token是否过期
	payLoad := tokenPayLoad{}
	base64Payload := token[firstNodeIndex+1 : lastNodeIndex]
	decodeString, _ := base64.URLEncoding.DecodeString(base64Payload)
	json.Unmarshal(decodeString, &payLoad)

	isExpire := VerifyExpire(payLoad.Exp)
	if !isExpire {
		return false, errors.New("token is expire")
	}

	return true, nil
}

// VerifyHS256 验证使用 HMAC SHA-256 || 实现:签名比对
func VerifyHS256(data, signature, secret string) (bool, error) {
	expectedSignature, err := SignHS256(data, secret)
	if err != nil {
		fmt.Println("gender signature by head and payload when check sign", err)
		return false, nil
	}

	// 比较签名,注意要处理 Base64 编码的比较问题
	return hmac.Equal([]byte(expectedSignature), []byte(signature)), nil
}

// VerifyExpire 验证token
func VerifyExpire(deadline int64) bool {
	return time.Unix(deadline, 0).After(time.Now())
}

// SignHS256 使用 HMAC SHA-256 算法对给定的数据进行签名,返回编码后的签名
func SignHS256(data, secret string) (string, error) {
	key := []byte(secret)
	h := hmac.New(sha256.New, key)
	_, err := h.Write([]byte(data))
	if err != nil {
		return "", err
	}
	hash := h.Sum(nil)
	return base64.URLEncoding.EncodeToString(hash), nil
}

注意:

以上代码的生成和校验过程是没有毛病的,我们可以将生成后的Token进行修改,就会返回token非法的错误。因为通过将收到的JWT的头部和载荷通过签名算法后得到的结果和接受到JWT的第三部分不匹配,这就说明有人篡改了我们的JWT。并且每个JWT都有一个有效时间,我们也能够对这一个有效时间判断是否过期。

但是由于Go本身的Base64Url编码和JWT官方采用的编码方式不太不一致,比如:JWT官方生成的Token会采用=代替_。所以,本文中生成的JWT如果采用直接在官网解析会出现错误,这主要是由编码方式不同产生的,属于具体的实现方式不同。

参考资料:

JWT官方文档:JSON Web Token 简介 - jwt.io

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值