记一次盲目copy代码导致的bug

2022/10/4 Go

最近在完善微服务网关,需要用Python生成JWT,而go去使用JWT。遇到了一个JSON反序列化的小问题,顺便记录一下。

# JWT

JWT想必大家都很熟悉了,我分别写过Java/Python和Go的版本,但大多数都是网上copy的demo。

这里奉劝一下大家,网上的demo代码很少能开箱即用,不是说没有哈,有些demo会存在一定的代码缺陷或者风险。copy代码,我们最后还得自己多测测多验证下,能跑就行是个误区。

今天就给大家聊聊我的经历。

# copy代码

下面是我从简书/csdn copy的源码,我也差不多看了一下,感觉没啥问题,token也能正常解析出来。

package auth

import (
	"encoding/json"
	"errors"
	"github.com/dgrijalva/jwt-go"
	"time"
)

var (
	TokenExpired     = errors.New("身份认证已过期, 请重新登录")
	TokenNotValidYet = errors.New("身份认证未生效")
	TokenMalformed   = errors.New("登录状态已失效, 请重新登录")
	TokenInvalid     = errors.New("身份认证不合法")
)

type UserInfo struct {
	ID    int    `json:"id"`    // userId
	Email string `json:"email"` // user email
	Name  string `json:"name"`  // username
	Role  int    `json:"role"`
}

type CustomClaims struct {
	UserInfo
	jwt.StandardClaims `json:"jwt,omitempty"`
}

func (c *UserInfo) Marshal() string {
	b, _ := json.Marshal(c)
	return string(b)
}

type JWT struct {
	signKey []byte
}

func NewJWT(key string) *JWT {
	return &JWT{[]byte(key)}
}

// GetSignKey get the sign key
func (j *JWT) GetSignKey() string {
	return string(j.signKey)
}

// SetSignKey set sign key for jwt
func (j *JWT) SetSignKey(key string) {
	j.signKey = []byte(key)
}

func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.signKey)
}

func (j *JWT) ParseToken(t string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(t, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.signKey, nil
	})
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				// Token is expired
				return nil, TokenExpired
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, TokenNotValidYet
			} else {
				return nil, TokenInvalid
			}
		}
		return nil, err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenInvalid
}

func (j *JWT) RefreshToken(tokenStr string) (string, error) {
	jwt.TimeFunc = func() time.Time {
		return time.Unix(0, 0)
	}
	token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.signKey, nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		jwt.TimeFunc = time.Now
		claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
		return j.CreateToken(*claims)
	}
	return "", TokenInvalid
}

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

上述代码里面,我仅仅只是加了一些错误类型。起初还是能正常运行的,到后面我发现一个token可以无限使用,也就是说token没有过期时间。然而我的pyjwt是能正常解析出过期时间的,这就必然说明go版本的代码存在问题了。

# 排查问题

# 确定问题

首先我们要做的就是Debug嘛,看看到底是哪里不对劲。JWT的数据内容一般分为2部分:

  • 系统自带的

    包括过期时间等等基础信息。

  • 用户定义的

    这里存放的就是真正的用户信息了。

    我们新建一个jwt_test.go文件:

package auth

import "testing"

func TestJWT_ParseToken(t *testing.T) {
	jt := NewJWT("你的token")
	jt.ParseToken("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjI0NDI1MTAsImlkIjoxLCJ1c2VybmFtZSI6Indvb2R5IiwibmFtZSI6Ilx1N2M3M1x1NmQxYiIsImVtYWlsIjoiNjE5NDM0MTc2QHFxLmNvbSIsInJvbGUiOjIsInBob25lIjoiMTg1MTY2MDA3MTYiLCJjcmVhdGVkX2F0IjoiMjAyMS0wMy0xNSAxNzowNDowMSIsInVwZGF0ZWRfYXQiOiIyMDIyLTA1LTA4IDAxOjE0OjMwIiwiZGVsZXRlZF9hdCI6MCwidXBkYXRlX3VzZXIiOjEsImxhc3RfbG9naW5fYXQiOiIyMDIyLTA5LTAzIDEzOjM1OjEwIiwiYXZhdGFyIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzExMjY2NzM4P3Y9NCIsImlzX3ZhbGlkIjp0cnVlfQ.4VM9f5mODxReoYQUXoTY_vX52lf-c8goOxnosy8o8Es")
}

1
2
3
4
5
6
7
8
9

接着我们只需要打下断点,查看信息即可:

可以看出,这个ExpireAt是0,也就是说没有正确解析到过期时间。这个也就是问题所在。

# 定位问题

问题已经确定好了,是expiredAt没有解析到,那么问题来了,我们是什么环节出了岔子呢?我们还得去源码里面一探究竟:

其实他就是一个JSON反序列化的操作,把我们的token字符串=>解密=>反序列化为JSON。

在Pyjwt里面,我们是用exp这个字段代表的过期时间,我们来看看反序列化的结构体里面是否也是这个字段:

可以看到完全对应,那就很奇怪了。到底问题出在哪了呢?

细看了CustomClaims之后,我发现了问题所在:

这里有个json标签(demo自带的源码),用它这个源码的话,生成的JSON会是这样的数据:

{"jwt": {"exp": 11111}, "role": 0, "email": "xxx.qq.com"}
1

也就是说,这个结构体内嵌了一个结构体,如果给他加上json标签的话,CustomClaims的字段就变成了 userInfo的字段+ jwt

去掉的话就是userInfo的字段+ StandardClaims的字段

而我们的pyjwt,exp字段和用户字段是同级,也就是下图所示:

这肯定解析不到,我们试下去掉这个tag:

type CustomClaims struct {
	UserInfo
	jwt.StandardClaims
}
1
2
3
4

# 重新尝试

再次debug,发现数据有了,那就是这个反序列化字段的问题。

# 总结

由于demo代码给内部struct加上了tag,导致内部struct有了自己的名字(jwt),所以序列化对应的数据变成了这样:

{"jwt": {"exp": "123"}}
1

所以反序列化也解析不出来数据。


最后的最后,警示大家,如果代码要上生产,切记一定要仔细测试/检查copy的残码