目录

Gin Jwt中间件

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