长短token设计

什么是长短token

长短token,即refresh tokenaccess token,在用户登录后,返回这两个token给用户,access token用于用户的每次请求的鉴权,refresh token用于用户的access token过期后,重新获取access token

使用场景

access token的有效期一般较短,为了避免用户在使用过程中token过期,需要使用refresh token来续期access token。通常access token的有效期为几分钟到几小时,refresh token的有效期为几天到几个月,这样可以做到即使access token泄露,也能保证一定的安全性。

其实一切需要token的地方都可以使用长短token,只是在不同的场景下,有效期和权限不同。

设计

本篇文章提供了一个简单的golang实现,使用jwt来生成access tokenrefresh token,实现了access token的过期和refresh token的续期,以及登出的功能。

对于登出功能,借助redis来实现,将refresh token存储在redis中,当用户登出时,删除refresh token

代码

  1. jwt生成与解析。将生成token的参数加上有效时间,这样我们可以同时生成access tokenrefresh tokenrefresh token不需要设置过期时间,借助redis来实现。(其实也可以给refresh token一个过期时间,从redis取出后解析一下,但是看实际业务中redis和鉴权哪个压力比较大吧)
package utils

import (
	"time"

	"github.com/dgrijalva/jwt-go"
)

type Claims struct {
	UserID int64 `json:"user_id"`
	jwt.StandardClaims
}

var Secret = []byte("tikmall")

// GenToken generates a jwt token with userID and role
// if duration is 0, the token will not expire, used for refresh token
func GenToken(userID int64, duration time.Duration) (string, error) {
	jwtCliams := new(jwt.StandardClaims)
	if duration != 0 {
		jwtCliams.ExpiresAt = time.Now().Add(duration).Unix()
	}

	c := Claims{
		userID,
		*jwtCliams,
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	return token.SignedString(Secret)
}

func ParseToken(tokenString string) (int64, error) {
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return 0, err
	}
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims.UserID, nil
	}
	return 0, err
}
  1. refresh token的存储与删除。这里使用redis来存储refresh token,当用户登出时,删除refresh token。续期用到了原子操作,内嵌了lua脚本。
func CacheToken(rdb *redis.Client, ctx context.Context, token string, userID int64) error {
	ttl := config.GetConf().Token.RefreshExpire

	return rdb.Set(ctx, utils.TokenKey(userID), token, time.Duration(ttl)*time.Second).Err()
}

func ExtendToken(rdb *redis.Client, ctx context.Context, userID int64) error {
	script := `
		if redis.call("EXISTS", KEYS[1]) == 1 then
			return redis.call("EXPIRE", KEYS[1], ARGV[1])
		else
			return 0
		end
	`

	ttl := config.GetConf().Token.RefreshExpire

	return rdb.Eval(ctx, script, []string{utils.TokenKey(userID)}, ttl).Err()
}

func DeleteToken(rdb *redis.Client, ctx context.Context, userID int64) error {
	return rdb.Del(ctx, utils.TokenKey(userID)).Err()
}

只需要在业务层调用这几个函数,就可以实现长短token的设计。在用户登录时,生成access tokenrefresh token,并将refresh token存储在redis中,返回给用户。在用户请求时,验证access token,如果过期,使用refresh token续期access token,如果refresh token过期,返回token过期错误。在用户登出时,删除refresh token