JWT令牌
记录一下jwt中间件的使用,似乎没有想象中的复杂
jwt介绍 jwt的构成官网已经写得很清楚了
包括签名算法、令牌类型
1
2
3
4
|
{
"alg": "HS256",
"typ": "JWT"
}
|
Payload
用户添加的、需要传递的数据,例如:用户信息、签发人、签发时间、过期时间等
1
2
3
4
5
|
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
|
Signature
把上面的header
+payload
+secrect
密钥进行签名得到,是为了确保数据完整性
1
2
3
4
5
6
|
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
|
定义Payload
Claims
定义的就是Payload部分
1
2
3
4
5
6
|
import "github.com/golang-jwt/jwt/v4"
type Claims struct {
UserID uint
jwt.RegisteredClaims
}
|
使用对称加密算法
对称加密算法就是使用同一个密钥进行签名和验签,算法可选HS256等,实现相对简单,网上例子也比较多。
发放Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func ReleaseToken(user *model.User) (string, error) {
expirationTime := time.Now().Add(time.Hour * time.Duration(expirationHour))
claims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "admin",
Subject: "user auth",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
|
用户登陆成功后就可以发放token,前端保留的token一般在有效期内都无需再次验证
解析Token
用户拿着刚签发的token发起请求,服务端就需要解析token
1
2
3
4
5
6
7
8
|
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
// 如果服务端的签名key不做任何处理的,直接返回jwtKey即可
return jwtKey, nil
})
return token, claims, err
}
|
中间件验证操作
解析了token以后,需要验证token的格式还有服务端要求的自定义校验等,如用户是否有效,token是否过期等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
func AuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 获取auth header
tokenString := ctx.GetHeader("Authorization")
// 验证格式
if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer") {
authFailResponse(ctx, errcode.ERR_TOKEN_INVALID)
return
}
// 验证token
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
token, claims, err := common.ParseToken(tokenString)
if err != nil || !token.Valid {
authFailResponse(ctx, errcode.ERR_TOKEN_INVALID)
return
}
// 验证用户是否还存在
var user model.User
db := common.GetDB()
err = db.Where("id=?", claims.UserID).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
authFailResponse(ctx, errcode.ERR_USER_NOT_EXISTS)
return
}
if err != nil {
authFailResponse(ctx, errcode.FAIL)
return
}
// 验证token是否过期
if !time.Now().Before(claims.ExpiresAt.Time) {
authFailResponse(ctx, errcode.ERR_TOKEN_EXPIRE)
return
}
// 验证发放token后用户是否有重置密码
if claims.IssuedAt.Time.Before(user.ResetAt) {
authFailResponse(ctx, errcode.ERR_TOKEN_RESET)
return
}
// 将用户信息写入上下文,从而在请求中可以通过ctx.Get()获取请求的用户信息
ctx.Set("user", user)
ctx.Next()
}
}
|
如果要避免用户重置密码后,旧token依旧有效的问题,需要在User Model
定义时增加一个ResetAt
时间字段,用来和签发时间比较,如果令牌签发时间比ResetAt
早说明这个Token已经无效
1
2
3
4
5
6
|
type User struct {
gorm.Model
Name string `gorm:"type:varchar(20);not null;unique" json:"name"`
...
ResetAt time.Time `gorm:"type:time" json:"reset_at"`
}
|
jwt中间件的时间精度应该保持与gorm一直,例如保持使用毫秒,避免时间比对时出现问题
1
|
jwt.TimePrecision = time.Millisecond
|
使用非对称加密算法
非对称加密算法就是签名和验签使用的是一对公私钥,私钥签名,公钥验签,算法可选RS256、Ed25519等等
生成公私钥
这里以Ed25519为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
// 生成公私钥到文件
func (au *Ed25519JwtAuth) genKey() error {
// 随机生成私钥密码
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
privateKeyDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return err
}
privateKeyBlock := pem.Block{
Type: "PRIVATE KEY",
Headers: nil,
Bytes: privateKeyDer,
}
privateKeyPem := pem.EncodeToMemory(&privateKeyBlock)
// 保存私钥到本地
err = os.WriteFile(au.privateKeyPath, privateKeyPem, 0600)
if err != nil {
return err
}
// 公钥
publicKeyDer, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return err
}
publicKeyBlock := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: publicKeyDer,
}
publicKeyPem := pem.EncodeToMemory(&publicKeyBlock)
// 保存公钥到本地
err = os.WriteFile(au.publicKeyPath, publicKeyPem, 0600)
if err != nil {
return err
}
return nil
}
// 从文件获取公私钥,其实就是genKey反向走一遍
func (au *Ed25519JwtAuth) getKey() error {
// 私钥
signBytes, err := os.ReadFile(au.privateKeyPath)
if err != nil {
return err
}
privateKeyBlock, _ := pem.Decode(signBytes)
signKey, err := x509.ParsePKCS8PrivateKey(privateKeyBlock.Bytes)
if err != nil {
return err
}
if key, ok := signKey.(ed25519.PrivateKey); ok {
au.signKey = key
} else {
return fmt.Errorf("sign key assert err")
}
// 公钥
verifyBytes, err := os.ReadFile(au.publicKeyPath)
if err != nil {
return err
}
publicKeyBlock, _ := pem.Decode(verifyBytes)
verifyKey, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
if err != nil {
return err
}
if key, ok := verifyKey.(ed25519.PublicKey); ok {
au.verifyKey = key
} else {
return fmt.Errorf("verify key assert err")
}
return nil
}
|
需要注意到是公私钥格式化方法Marshalxxx
和解析方法Parsexxx
要对应,否则解析时会抛出x509: malformed tbs certificate
之类的错误
发放Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 和对称加密过程基本没区别
func (au *Ed25519JwtAuth) ReleaseToken(user *model.User) (string, error) {
expirationTime := time.Now().Add(time.Hour * time.Duration(au.expiredHour))
claims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "admin",
Subject: "user auth",
},
}
// 使用EdDSA签名方案
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
// 私钥签名
tokenString, err := token.SignedString(au.signKey)
if err != nil {
return "", err
}
return tokenString, nil
}
|
解析Token
1
2
3
4
5
6
7
8
|
func (au *Ed25519JwtAuth) ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
// 公钥验签
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return au.verifyKey, nil
})
return token, claims, err
}
|
一些参考文章
RSA、DSA、ECDSA、EdDSA 和 Ed25519 的区别
RS256签发JWT