使用jwt-go实现jwt签名与验签
背景: jwt (json web token) 被广泛地用于各种登录, 授权场景。看完此教程, 你将有能力理解什么是jwt, 区别对称与非对称的jwt签名与验签方式, 并使用go语言实现两种方式(HS256与RS256)的jwt签名与验签。
什么是 jwt ?
简单来说, jwt 就是一个令牌。客户端有了这个令牌, 就可以拿着它去服务端获取一些资源。它大概长这样:
这样看你可能看不出是什么东西, 你只能看到这是一长串字符, 被"."分成了3个部分。我们不妨把它整理一下, 比如访问jwt.io/ 这个网站, 把上面这一串字符贴上去,你将看到下面这样:
事实上, 那一长串字符由3部分组成(被两个"."分成了3个给部分)。
第一部分是header, 是一个json字符串, 描述了自己是什么和生成的算法。比如这里的header表示这是 jwt 并且使用了 RS256 算法签名。这一串字符是经过base64编码后的结果, 大家可以访问www.base64decode.org/ 把它贴上去, 看看decode出来的是不是上面图片的 json 字符串。
第二部分是 payload, 也就是令牌的内容, 也是一个json字符串。这里面具体有什么字段是可以自定义的, 在后面我会详细介绍一般jwt都有哪些字段。大家这里只要知道这是令牌的内容就行了。
第三部分是签名。所谓签名, 就是对第二部分的payload进行取哈希值。由于哈希函数具有 单向性和 很强的方碰撞性,所以可以防止有人串改第二部分的 payload。
当接受jwt的一方需要对签名进行验证, 这个过程叫做验签。只有经过验签的jwt才是有效和真实的。
jwt的签名和验签都需要密钥的参与,要不然谁都可以生成一个 jwt, 服务端无法确认 jwt 的真实性,也就无法使用了。具体如何签名我在下面详细介绍
常见的 签名 和验签 有哪些算法?
按照密钥的类型来分, 有两种, 分别是对称式签名(验签) 和非对称式签名(验签)。
对称式签名(验签)
对称式最常见的当属 HS256。简单来说, 签名过程 就是对 header,payload和密钥的拼接 进行一次取SHA256哈希值, 作为 jwt 的第三部分。 用公式来写, 就是这样:
验签过程需要拿到 密钥 key(提前约定好的), 根据 header 和 payload 计算SHA256哈希值,如果和 jwt 的第三部分一致, 说明 jwt 真实, 否则说明 jwt 被篡改。
非对称式签名(验签)
非对称式最常见的当属 RS256。简单来说, 就是对 header 和 payload 计算SHA256哈希值, 随后对这个哈希值使用私钥进行加密。 用公式来写就是这样:
验签的时候使用公钥解密出 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 的 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}}
巨人的肩膀:
转载自:https://juejin.cn/post/7219651766706159653