将token加入到整个系统中 | 青训营笔记

170 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第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.WaitGroupchan,其中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, ""
}