0%

golang笔记-jwt的使用

JWT在微服务中的认证中经常使用,本文记录jwt搭配gin来给app的api接口提供基本的认证服务,首先介绍jwt的封装,然后介绍实现gin认证的中间件以提供jwt的认证,最后介绍一下token过期时,token的刷新过程。

这里需要注意一些问题,比如出于安全考虑,短token一般有时间限制在2小时,长token一般在15~30天不等。jwt也考虑在不同微服务间提供共享的认证服务所需要注意的问题。

1.封装golang-jwt的使用

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// JWT基本数据结构
// 签名的signkey
type JWT struct {
SigningKey []byte
TokenTime time.Duration
}

// 定义载荷
type CustomClaims struct {
// StandardClaims结构体实现了Claims接口(Valid()函数)
jwt.RegisteredClaims

Uuid uint64 `json:"uuid"`
Username string `json:"userName"`
}

// 初始化JWT实例
func NewJWT() *JWT {
return &JWT{
[]byte(GetSignKey()),
model.TokenAliveTime,
}
}

// 获取signkey(这里写死成一个变量了)
func GetSignKey() string {
return SignKey
}

func SetSignKey(key string) string {
SignKey = key
return SignKey
}

// 创建Token(基于用户的基本信息claims)
// 使用HS256算法进行token生成
// 使用用户基本信息claims以及签名key(signkey)生成token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#Token
// 返回一个token的结构体指针
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}

// token解析
// Couldn't handle this token:
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ParseWithClaims
// 输入用户自定义的Claims结构体对象,token
// 以及自定义函数来解析token字符串为jwt的Token结构体指针
// Keyfunc是匿名函数类型: type Keyfunc func(*Token) (interface{}, error)
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{},
func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})

//fmt.Println(token, err)
if err != nil {
// https://gowalker.org/github.com/dgrijalva/jwt-go#ValidationError
// jwt.ValidationError 是一个无效token的错误结构
if ve, ok := err.(*jwt.ValidationError); ok {
// ValidationErrorMalformed是一个uint常量,表示token不可用
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
// ValidationErrorExpired表示Token过期
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, TokenExpired
// ValidationErrorNotValidYet表示无效token
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}

}
}

// 将token中的claims信息解析出来和用户原始数据进行校验
// 做以下类型断言,将token.Claims转换成具体用户自定义的Claims结构体
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}

return nil, TokenInvalid

}

// 更新Token
func (j *JWT) UpdateToken(tokenString string) (string, error) {
// TimeFunc为一个默认值是time.Now的当前时间变量,用来解析token后进行过期时间验证
// 可以使用其他的时间值来覆盖
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}

// 拿到token基础数据
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{},
func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil

})

// 校验token当前还有效
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
// 修改Claims的过期时间(int64)
// https://gowalker.org/github.com/dgrijalva/jwt-go#StandardClaims
now := time.Now().Add(model.TokenAliveTime)
claims.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(now)
return j.CreateToken(*claims)
}
return "", fmt.Errorf("token获取失败:%v", err)
}

2. 给gin提供jwt认证中间件

这里要注意如果是token过期,返回错误401,对应app拿长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
// JWTAuth 中间件,检查token
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
result := &model.ResultInfo{Result: model.ESystemErr}
token := c.Request.Header.Get("token")
if token == "" {
result.Result = model.EInvalidToken
c.JSON(http.StatusOK, result)
c.Abort()
return
}

j := NewJWT()
// 解析token中包含的相关信息
claims, err := j.ParserToken(token)

if err != nil {
// token过期
if err == TokenExpired {
result.Result = model.EExpireTimeOut
c.JSON(http.StatusUnauthorized, result)
c.Abort()
log.Errorf("JWTAuth error,timeout token:%s", token)
return
}
// 其他错误
log.Errorf("JWTAuth error,token error:%s", token)
result.Result = model.ETokenErr
c.JSON(http.StatusOK, result)
c.Abort()
return
}

// 解析到具体的claims相关信息
c.Set("claims", claims)
//c.Set("uid", claims.Uuid)
}
}

在gin中增加JWTAuth
1
2
3
4
5
func initUserRouter(p_groups *gin.RouterGroup) {
groups := p_groups.Group("/user")
groups.Use(md.JWTAuth())
// ...
}

3. 生成token.

注意:首次登录时要么用户名密码登录,要么第三方认证登录之后,签发本地token,这个jwt的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
// token生成器
func GenerateToken(info *ResultInfo, account *Account) error {
// 构造SignKey: 签名和解签名需要使用一个值
j := md.NewJWT()
now := time.Now().Add(model.TokenAliveTime)

// 构造用户claims信息(负荷)
claims := md.CustomClaims{
jwt.RegisteredClaims{
Issuer: "chessbooks.cn",
Subject: "chess",
ExpiresAt: jwt.NewNumericDate(now),
ID: strconv.Itoa(jwtGenteratorId),
},
account.MemberId,
account.Username,
}

// 根据claims生成token对象
token, err := j.CreateToken(claims)
if err != nil {
return err
}

rtoken, err := generateRefreshToken(account.MemberId)
if err != nil {
return err
}

info.Token = token
info.RefreshToken = rtoken
info.UUID = strconv.FormatUint(account.MemberId, 10)
return nil
}

jwt使用注意事项:

  • 首次登录时,需要颁发短token和长token,一般认证需要用的是短token,短token如果泄漏,那么安全边界就是下一次刷新token时,用户因为没有长token而不能续签。这里说明的问题是jwt的token是无状态的,泄漏的token,后台不能撤销token,只能等待token的自动过期。所以部署微服务一般强制要求使用https。
  • 短token过期时,使用长token去刷新短token。
  • 长token过期时,要求用户重新登录。(有人说要自动续长token的时间,我在这里建议尽量不要开这个口子,长token30天期限重新登录,体验上也没有什么区别。)
  • 登录不能代替认证,jwt的颁发是服务器自颁发和防篡改的,由于token有可能被劫持的关系,类似拿着你身份证的人不一定是你自己,所以登录认证和token并不是一一对应。