背景: jwt (json web token) 被广泛地用于各种登录, 授权场景。看完此教程, 你将有能力了解什么是jwt, 差异对称与非对称的jwt签名与验签方法, 并运用go语言完成两种方法(HS256与RS256)的jwt签名与验签。

什么是 jwt ?

简略来说, jwt 便是一个令牌。客户端有了这个令牌, 就可以拿着它去服务端获取一些资源。它大概长这样:

这样看你或许看不出是什么东西, 你只能看到这是一长串字符, 被”.”分成了3个部分。咱们不妨把它收拾一下, 比方拜访jwt.io/ 这个网站, 把上面这一串字符贴上去,你将看到下面这样:

使用jwt-go实现jwt签名与验签

事实上, 那一长串字符由3部分组成(被两个”.”分成了3个给部分)。

第一部分是header, 是一个json字符串, 描述了自己是什么和生成的算法。比方这里的header表明这是 jwt 而且运用了 HS256 签名算法。这一串字符是通过base64编码后的成果, 咱们可以拜访www.base64decode.org/ 把它贴上去, 看看decode出来的是不是上面图片的 json 字符串。

第二部分是 payload, 也便是令牌的内容, 也是一个json字符串。这里面具体有什么字段是可以自定义的, 在后面我会具体介绍一般jwt都有哪些字段。咱们这里只需知道这是令牌的内容就行了。

第三部分是签名。所谓签名, 便是对第二部分的payload进行取哈希值。因为哈希函数具有 单向性和 很强的方磕碰性,所以可以防止有人串改第二部分的 payload。

当承受jwt的一方需求对签名进行验证, 这个进程叫做验签。只要通过验签的jwt才是有效和实在的。

jwt的签名和验签都需求密钥的参加,要不然谁都可以生成一个 jwt, 服务端无法承认 jwt 的实在性,也就无法运用了。具体如何签名我在下面具体介绍

常见的 签名 和验签 有哪些算法?

依照密钥的类型来分, 有两种, 分别是对称式签名(验签) 和非对称式签名(验签)。

对称式签名(验签)

对称式最常见的当属 HS256。简略来说, 签名进程 便是对 header,payload和密钥的拼接 进行一次取SHA256哈希值, 作为 jwt 的第三部分。 用公式来写, 便是这样:

signedString=SHA256(header+payload+key)signedString = SHA256(header+payload+key)

验签进程需求拿到 密钥 key(提前约定好的), 根据 header 和 payload 核算SHA256哈希值,假如和 jwt 的第三部分共同, 阐明 jwt 实在, 不然阐明 jwt 被篡改。

非对称式签名(验签)

非对称式最常见的当属 RS256。简略来说, 便是对 header 和 payload 核算SHA256哈希值, 随后对这个哈希值运用私钥进行加密。 用公式来写便是这样:

signedString=SHA256(header+payload)signedString = SHA256(header+payload )
cipherText=RSAencrypt(signedString,privateKey)cipherText =RSA_{encrypt}(signedString, privateKey)

验签的时分运用公钥解密出 signedString, 再根据 header 和 payload 核算SHA256哈希值, 随后对这个哈希值和 jwt的部分进行比对。假如和 jwt 的第三部分共同, 阐明 jwt 实在, 不然阐明 jwt 被篡改。

HS256 与 RS256 差异

HS256 需求两边严厉保管密钥, 假如有一方走漏了密钥, 那么就可以伪造出 jwt. 而 RS256 签名的时分运用私钥, 验签的时分运用公钥,只需私钥不走漏, 那么jwt是不能被伪造的, 充其量只是公钥走漏, 谁都验证jwt罢了。

运用 go-jwt 完成 jwt 的签名与验签

创立新目录 jwt_demo 在该目录下打开命令行输入:

go mod init jwt_demo
go mod tidy

随后新建两个目录: RS256 与 HS256, 整个工程目录如下:

oauth_demo
    --HS256
        --HS256.go
    --RS256
        --RS256.go
    --go.mod  
        --go.sum

jwt 的 HS256签名与验签:

package main
import (
   "errors"
   "fmt"
   "github.com/golang-jwt/jwt/v5"
   "math/rand"
   "time"
)
type MyCustomClaims struct {
   UserID     int
   Username   string
   GrantScope string
   jwt.RegisteredClaims
}
// 签名密钥
const sign_key = "hello jwt"
// 随机字符串
var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randStr(str_len int) string {
   rand_bytes := make([]rune, str_len)
   for i := range rand_bytes {
      rand_bytes[i] = letters[rand.Intn(len(letters))]
   }
   return string(rand_bytes)
}
func generateTokenUsingHs256() (string, error) {
   claim := MyCustomClaims{
      UserID:     000001,
      Username:   "Tom",
      GrantScope: "read_user_info",
      RegisteredClaims: jwt.RegisteredClaims{
         Issuer:    "Auth_Server",                                   // 签发者
         Subject:   "Tom",                                           // 签发目标
         Audience:  jwt.ClaimStrings{"Android_APP", "IOS_APP"},      //签发受众
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),   //过期时刻
         NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second)), //最早运用时刻
         IssuedAt:  jwt.NewNumericDate(time.Now()),                  //签发时刻
         ID:        randStr(10),                                     // wt ID, 类似于盐值
      },
   }
   token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim).SignedString([]byte(sign_key))
   return token, err
}
func parseTokenHs256(token_string string) (*MyCustomClaims, error) {
   token, err := jwt.ParseWithClaims(token_string, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
      return []byte(sign_key), nil //回来签名密钥
   })
   if err != nil {
      return nil, err
   }
   if !token.Valid {
      return nil, errors.New("claim invalid")
   }
   claims, ok := token.Claims.(*MyCustomClaims)
   if !ok {
      return nil, errors.New("invalid claim type")
   }
   return claims, nil
}
func main() {
   token, err := generateTokenUsingHs256()
   if err != nil {
      panic(err)
   }
   fmt.Println("Token = ", token)
   time.Sleep(time.Second * 2)
   my_claim, err := parseTokenHs256(token)
   if err != nil {
      panic(err)
   }
   fmt.Println("my claim = ", my_claim)
}

一开始定义了一个 MyCustomClaims 结构体, 这其实便是自定义的 payload. 其中的 UserID, Username, GrantScope 字段便是咱们自定义的内容, 一般与身份和授权信息有关。MyCustomlaims里面嵌套的 RegisteredClaim 便是规范的 jwt payload。

下面来看MyCustomClaims 里面各字段的意义。

字段 意义
UserId 000001 自定义字段, 用户ID, 表明这个 jwt 效果于特定用户
UserName Tom 自定义字段, 用户名, 表明这个 jwt 效果于特定用户
GrantScope read_user_info 自定义字段, 授权规模, 标识这个 jwt 可以干啥
Issuer Auth_Server 规范字段, jwt 签名方, 表明是谁签发的这个 jwt
Subject Tom 规范字段, 表明这个 jwt 效果目标, 在这里与 Username 等效, 再写一遍方便了解
Audience jwt.ClaimStrings{“Android_APP”, “IOS_APP”} 规范字段, 表明jwt 签发给谁, 比方后端某个服务(Auth_Server)签发给客户端(Android_APP, IOS_APP)运用
ExpiresAt jwt.NewNumericDate(time.Now().Add(time.Hour)) 规范字段, jwt 过期时刻点
NotBefore jwt.NewNumericDate(time.Now().Add(time.Hour)) 规范字段, jwt 最早的有效时刻点, 早于这个时刻点无效
IssuedAt jwt.NewNumericDate(time.Now().Add(time.Hour)) 规范字段, jwt 的签发时刻点
ID 随机数 规范字段, jwt的ID, 尽量仅有, 我了解为类似于在Hash之前加盐值, 愈加防磕碰

在 main 函数中我特意休眠了 两秒, 为的便是到达 MotBefore 之后的时刻, 假如咱们注释应该会报错。

运行成果:

Token =  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJuYW1lIjoiVG9tIiwiR3JhbnRTY29wZSI6InJlYWRfdXNlcl9pbmZvIiwiaXNzIjoiQXV0aF9TZXJ
2ZXIiLCJzdWIiOiJUb20iLCJhdWQiOlsiQW5kcm9pZF9BUFAiLCJJT1NfQVBQIl0sImV4cCI6MTY4MDk1MDQ4OSwibmJmIjoxNjgwOTQ2ODkwLCJpYXQiOjE2ODA5NDY4ODksImp0aSI6IkR6
ZzlNZ1NlUFIifQ.7Yi42Ur2Yivh5dpmMY-CxpQ5kR0IoIAh7F8xNLjdAcM
my claim =  &{1 Tom read_user_info {Auth_Server Tom [Android_APP IOS_APP] 2023-04-08 18:41:29 +0800 CST 2023-04-08 17:41:30 +0800 CST 2023-04-08 
17:41:29 +0800 CST Dzg9MgSePR}}

咱们在回到 jwt.io/ 这个网站, 把签名密钥 hello jwt 放入右下角 的 VERIFY SIGNATURE 中去,你会发现左下角的 Invalid Signature 变成了Signature Verified,阐明验签成果是 jwt 有效。

使用jwt-go实现jwt签名与验签

jwt 的 RS256签名与验签:

package main
import (
   "crypto/rsa"
   "crypto/x509"
   "encoding/pem"
   "errors"
   "fmt"
   "github.com/golang-jwt/jwt/v5"
   "math/rand"
   "time"
)
type MyCustomClaims struct {
   UserID     int
   Username   string
   GrantScope string
   jwt.RegisteredClaims
}
// 随机字符串
var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randStr(str_len int) string {
   rand_bytes := make([]rune, str_len)
   for i := range rand_bytes {
      rand_bytes[i] = letters[rand.Intn(len(letters))]
   }
   return string(rand_bytes)
}
// pkcs1
func parsePriKeyBytes(buf []byte) (*rsa.PrivateKey, error) {
   p := &pem.Block{}
   p, buf = pem.Decode(buf)
   if p == nil {
      return nil, errors.New("parse key error")
   }
   return x509.ParsePKCS1PrivateKey(p.Bytes)
}
const pri_key = `-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAL00QsML/ovZle3Lq3C7QBo9s00ivsLhG2xlamhHOZDrjTGJX4OA
H27qQbDREcYXpUt5JqOt+KzB4MA/vUKCbT0CAwEAAQJBAINbkS5RWXxGqCzcRj6S
AkM1qxJWmRI7rwpmrqWPLYxKiS1i/i3bwSA3H+NODWIk1p2BWtycWzx5s3cNLn4b
gIECIQD6WuNzXxZHRIxRJQDRyEeWLsrRv9nkZJXHde78DoIZuQIhAMF4ZOgQX2hV
+y9YZmca2tW7etwGPmVjFWQd6JFtjyGlAiBFR9GZo76uijGqYusPIrVswhYuZUEP
CybHw8MWzY0DQQIgc4DDDWCo9QtP+MYX7Lo1p6BUCwOXQMRUwv6wGBKGfxkCIQDn
EKF3Ee6bnLT5DMfrnGY20RNg1Yes+14KkEyYsx0++Q==
-----END RSA PRIVATE KEY-----
`
const pub_key = `-----BEGIN RSA PUBLIC KEY-----
MEgCQQC9NELDC/6L2ZXty6twu0AaPbNNIr7C4RtsZWpoRzmQ640xiV+DgB9u6kGw
0RHGF6VLeSajrfisweDAP71Cgm09AgMBAAE=
-----END RSA PUBLIC KEY-----
`
func generateTokenUsingRS256() (string, error) {
   claim := MyCustomClaims{
      UserID:     000001,
      Username:   "Tom",
      GrantScope: "read_user_info",
      RegisteredClaims: jwt.RegisteredClaims{
         Issuer:    "Auth_Server",                                   // 签发者
         Subject:   "Tom",                                           // 签发目标
         Audience:  jwt.ClaimStrings{"Android_APP", "IOS_APP"},      //签发受众
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),   //过期时刻
         NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second)), //最早运用时刻
         IssuedAt:  jwt.NewNumericDate(time.Now()),                  //签发时刻
         ID:        randStr(10),                                     // jwt ID, 类似于盐值
      },
   }
   rsa_pri_key, err := parsePriKeyBytes([]byte(pri_key))
   token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claim).SignedString(rsa_pri_key)
   return token, err
}
func parsePubKeyBytes(pub_key []byte) (*rsa.PublicKey, error) {
   block, _ := pem.Decode(pub_key)
   if block == nil {
      return nil, errors.New("block nil")
   }
   pub_ret, err := x509.ParsePKCS1PublicKey(block.Bytes)
   if err != nil {
      return nil, errors.New("x509.ParsePKCS1PublicKey error")
   }
   return pub_ret, nil
}
func parseTokenRs256(token_string string) (*MyCustomClaims, error) {
   token, err := jwt.ParseWithClaims(token_string, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
      pub, err := parsePubKeyBytes([]byte(pub_key))
      if err != nil {
         fmt.Println("err = ", err)
         return nil, err
      }
      return pub, nil
   })
   if err != nil {
      return nil, err
   }
   if !token.Valid {
      return nil, errors.New("claim invalid")
   }
   claims, ok := token.Claims.(*MyCustomClaims)
   if !ok {
      return nil, errors.New("invalid claim type")
   }
   return claims, nil
}
func main() {
   token, err := generateTokenUsingRS256()
   if err != nil {
      panic(err)
   }
   fmt.Println("Token = ", token)
   time.Sleep(time.Second * 2)
   my_claim, err := parseTokenRs256(token)
   if err != nil {
      panic(err)
   }
   fmt.Println("my claim = ", my_claim)
}

其中的RSA公私钥对可以运用openssl生成, 也可以拜访这个网站 www.metools.info/code/c80.ht… 在线生成。

运行成果:

Token =  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEsIlVzZXJuYW1lIjoiVG9tIiwiR3JhbnRTY29wZSI6InJlYWRfdXNlcl9pbmZvIiwiaXNzIjoiQXV0aF9TZXJ
2ZXIiLCJzdWIiOiJUb20iLCJhdWQiOlsiQW5kcm9pZF9BUFAiLCJJT1NfQVBQIl0sImV4cCI6MTY4MDk2NTg3MSwibmJmIjoxNjgwOTYyMjcyLCJpYXQiOjE2ODA5NjIyNzEsImp0aSI6ImxY
UXU0VE9YUEoifQ.OEBgs3UU7WqUafaxyBnhgA5Mb2WU6E9-5GtRB2nQ3zHvEU1RF3c9AVMbsSkIFUORZVG8bcVe8-JyVR0fpKEsAA
my claim =  &{1 Tom read_user_info {Auth_Server Tom [Android_APP IOS_APP] 2023-04-08 22:57:51 +0800 CST 2023-04-08 21:57:52 +0800 CST 2023-04-08 
21:57:51 +0800 CST lXQu4TOXPJ}}

巨人的膀子:

www.rfc-editor.org/rfc/rfc8017

github.com/golang-jwt/…

en.wikipedia.org/wiki/JSON_W…