长短token设计
什么是长短token
长短token,即refresh token
和access 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 token
和refresh token
,实现了access token
的过期和refresh token
的续期,以及登出的功能。
对于登出功能,借助redis
来实现,将refresh token
存储在redis
中,当用户登出时,删除refresh token
。
代码
jwt
生成与解析。将生成token
的参数加上有效时间,这样我们可以同时生成access token
和refresh token
,refresh 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
}
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 token
和refresh token
,并将refresh token
存储在redis
中,返回给用户。在用户请求时,验证access token
,如果过期,使用refresh token
续期access token
,如果refresh token
过期,返回token
过期错误。在用户登出时,删除refresh token
。