这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记。
我在本次青训营中参与的组队大项目是极简版抖音的服务端实现。本篇笔记主要讨论如何将JWT有机地融于当前系统中。
在使用JWT时,需要考虑token的生成(或者叫签发)、校验。
token签发的时机
在用户通过用户名/密码的方式通过登录验证后,可以签发token。token使用到的userID、username、secret通过底层存储取得,本系统中取得userID和username使用的是MySQL数据库,取得secret使用的是Redis。
此处,为了进一步增加secret的随机性,采取如下方式:(1)生成具有x个随机固定长度字符串的数组;(2)在签发token、需要获得secret时,任意返回其中一个作为secret。
对于数组,由于不顾及相对与绝对顺序,所以可以考虑使用Redis中的Set这一数据结构来进行实现,使用SADD指令向以”salt“为key的集合中,加入生成的随机数组。
为了复用secret,在系统初始化时就一次性将secret设置好。
设置secret的具体代码如下:
func createRandomString(count int) []string {
var randStrs []string
for i := 1; i <= count; i++ {
wordList, _ := generate.GenRandomMix(10)
randStrs = append(randStrs, wordList.Word)
}
return randStrs
}
func setSalts() {
salts := GetAllSalts()
if len(salts) != 0 {
logx.DyLogger.Infof("salts already exist!")
return
}
err := rdb.SAdd(keySalt, createRandomString(10)).Err()
if err != nil {
logx.DyLogger.Panicf("set salts error, err=%+v", err)
}
return
}
如何利用token完成校验
过期时间校验
根据需求,确定过期时间。一般来说,安全性比较高的app(比如掌上银行等),都会设置一个固定的token过期时间,即过了这段时间后token将失效。在安全性允许的前提下,还可以采取续签的手段,用于刷新token的过期时间,此处需要客户端借助另一个token配合实现,本系统客户端较为简易且请求逻辑固定,所以本篇文章暂不讨论。Redis中的key天然具有过期时间这一特性,所以直接使用Redis的这一特性即可实现token过期校验。
格式合法性校验
服务端直接基于token中Header指定的签名算法,尝试使用secret进行验签,若成功则格式合法。但此时secret是一个存放随机字符串的数组,所以可以采用goroutine协程并发的方式,尝试将数组中每个字符串都作为真正的secret进行验签,如有成功则为真正的secret。此时因为借助并发,所以还需要使用到sync.WaitGroup和chan,其中channel的缓冲区大小应设置为1,因为数组中最终只有一个随机字符串会成功验签。在验签成功后,可以从Payload中取得相应的userID和username。
取得secret的代码如下:
func GetAllSalts() []string {
return rdb.SMembers(keySalt).Val()
}
解析token的代码如下:
func ParseToken(tokenJson string) (int64, string) {
if tokenJson == "" {
return InvalidUserId, ""
}
salts := rdb.GetAllSalts()
result := make(chan jwt.MapClaims, 1)
var wg sync.WaitGroup
for index := 0; index < len(salts); index++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
token, err := jwt.Parse(tokenJson, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(salts[index]), nil
})
if err == nil && token.Valid {
claims, _ := token.Claims.(jwt.MapClaims)
result <- claims
}
}(index)
}
wg.Wait()
for item := range result {
if item != nil {
return int64(item[userId].(float64)), item[username].(string)
}
}
return InvalidUserId, ""
}