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的生成过程:
-
标头经过 Base64Url 编码成为JWT的第一个部分。
-
对有效负载进行 Base64Url 编码,形成JWT的第二部分。
-
获取到标头中指定的签名算法,然后对标头信息、有效负载信息、密钥进行签名。
-
最后,我们将这三个部分的字符串使用.进行拼接,形成上方格式的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如果采用直接在官网解析会出现错误,这主要是由编码方式不同产生的,属于具体的实现方式不同。