gin框架使用jwt, 双token刷新,续期

双token刷新、续期,access_token和refresh_token实效如何设置

背景

token 认证,生成的 token 过一段时间就会失效(不要故意把时间设的很长,这样不安全,token 变的毫无意义)用户需要重新登录获取 token。用户经常使用客户端,使用的过程中由于 token 到期,客户端跳到登录界面要求登录,这样体验太差了!  比如:token 有效期是 2h,用户一直在使用客户端,使用的过程中,token 到期跳转到登录页面邀请重新登录。第一次忍了,过了2个小时又要重新登录! 用户:MDZZ,再见。

 

解决问题

为了解决上述 token 过期了活跃用户需要再次重新登录的问题,我们需要 token 刷新。

需要了解的一个问题:什么时候需要用户跳转到登录页面重新登录?

token过期了就需要用户跳转登录页面重新登录? 显然不是的,如果是 不活跃用户 token过期了,确实需要跳转登录页面重新登录。 但是 活跃用户 ,就算token过期了,也不应该跳转登录页面。

 

活跃用户

在 access_token 创建开始时间点 到 2 * access_token 实效的时间内认为用户是活跃的。

解释:

 access_token 有效时长是 et

 活跃用户时长 at, 即用户在一次正常操作客户端后的 at 时间内都是活跃。(at >= et, 用户 access_token 在有效期被我们认为是活跃)

为什么 at >=et ?

  假设 at =et

  当用户登录后at = et 能满足: 用户access_token在有效期et内我们认为用户是活跃
当用户在登录后access_token结束前的某个点操作一次客户端后, at = et 还是能满足: 用户access_token在有效期et内我们认为用户是活跃 。

  

 

 

 

  假设at > et

  当用户登录后at > et 能满足: 用户access_token在有效期et内我们认为用户是活跃
当用户在登录后access_token结束前的某个点操作一次客户端后, at > et 还是能满足: 用户access_token在有效期et内我们认为用户是活跃。

 

 

 

  假设at < et

  当用户登录后at < et 不能满足: 用户access_token在有效期et内我们认为用户是活跃 。所以不成立

  

 

 

 由上3个假设 at=et成立,at>et成立,at<et不成立,得知 at>=et

 

结论

若access_token有效期时长et ,活跃用户时长at ,那么有 at >=et ,且用户每次正常操作客户端后用户活跃时间应刷新(即用户一次正常操作客户端后的at时间内都是活跃的), 所以可以认为 [access_token创建开始时间点 ,2*access_token有效时长 ] 时间内用户是活跃的

双token的刷新 access_token和refresh_token

第一次用账号密码登录服务器会返回两个 token : access_token 和 refresh_token,时效长短不一样。短的access_token 时效过了之后,发送时效长的 refresh_token 重新获取一个短时效token,如果都过期,就需要重新登录了。

 refresh_token 就是用来刷新access_token 。活跃用户的 access_token 过期了,用refresh_token 获取 新的access_token 。
 

access_token 和 refresh_token 的有效时间如何设置

为了保证能够刷新活跃用户的access_token , refresh_token 的有效时间 不能小于 用户活跃时间点
假设 access_token 有效时间是 et ,那么用户在 [ access_token 起始时间点 ,2*et ] 时间内用户是活跃的 ,由此可知 refresh_token 的有效时间 >= 2 * access_token 的有效时间

一般,refresh_token 的有效时间 > 2 * access_token 的有效时间

比如,access_token 实效7天,那么 refresh_token 实效可以给15天,也可以给30天
当然,access_token和refresh_token 的时长具体多少,需要根据环境决定,如涉及到金钱的 银行客户端,12306客户端等 token时长都很短,而普通app客户端的token可以是几天甚至上月.

刷新refresh_token 

每次 刷新access_token 时判断 refresh_token 是否快过期,如果是,那就连refresh_token 也刷新。

如果希望降低 刷新refresh_token 频率,可以将 refresh_token 实效提高

 

 

下面是一问网友疑惑问我的解答过程,很多朋友应该都有类似的疑惑,特意放在这里供参考 (

复制代码
10:19:41
如果不用refreshToken,只用一个AccessToken,然后把accessToken设置过期时间为refreshToken的过期时间,如果accessToken快过期,直接返回新的accessToken,这样不是更方便吗

答 10:20:45
这样就相当于 每个 accessToken 时效时间 就会要求必须重新登录

答 10:21:22
有用户会反感的,你自己用这种app 不会反感吗  心想 又要登录

答 10:22:06
我天天使用你的app 难道不知道我是活跃用户那 为什么还要我登录

答 10:22:14
你自己想想 是不是这个道理

问 10:22:27
不是,我的意思是使用快过期的accessToken换取新的token,相当于把accessToken和refreshToken合并

问 10:23:06
不用用户输入账号密码,他吧快过期的token给我,我自动检测过期时间,给他返回新的token

答 10:23:06
那 这就是你没有理解 什么是活跃用户

答 10:25:22
你如何判断 人家块过期了

问 10:26:16
比如过期时间是3小时,如果两小时了,还差一小时(1/3的比例),那么就刷新token,和refreshToken的机制类似

答 10:26:46
你怎么确定用户会在 这个时间段登录?

答 10:26:54
如果没在这个时间段登录呢

问 10:27:05
那就失效了

答 10:27:10
我在3.01登录

问 10:27:18
不过refreshToken不是也会存在这个问题吗

答 10:27:19
但是我在1小时的时候登录过

答 10:27:50
你要理解什么是活跃

答 10:28:17
我在某个时间点操作过,那么这之后的 固定时间段 我都算活跃

答 10:28:31
这个活跃时间 都不应该让我再次登录

答 10:28:45
即使access_token过期

答 10:29:11
一个token 是无法保证这个的,所以才会有 refreshtoken

答 10:30:10
你考虑问题没考虑全,refresh存在肯定是有道理的,试着吧各个问题都考虑到

问 10:30:28
嗯嗯,我再思考一下~10:32:34
我在 1小时登录过, 然后在3.01小时登录 ,按你的说法 我3.01这个时间登录失效了, 但是我在1小时登录过,那么 我在1小时到4小时内 都算是活跃用户,那么凭什么我3.01 登录说我过期了

答 10:34:14
理解 我文中提到的活跃用户概念,我在某个时间操作 那么之后固定时间内(比如你这里的3小时)我都是活跃的 不管accesstoken 是否过期(与我用户有什么关系,用户又不管这个)

问 10:39:08
如果用refreshToken,有效期不也是到3小时也失效了吗

答 10:39:41
按你的需求  refreshToken 可以设置为7个小时

答 10:40:09
这样就可以保证用户 任意一次操作后的3小时内 一定不会出现 过期情况

问 10:43:49
 双Token方案:
    accessToken过期时间: 3小时
    refreshToken过期时间:7小时
    刷新refreshToken时机:剩余有效期小于6小时
    
 单Token方案:
    accessToken过期时间:7小时
    刷新accessToken时机:剩余有效期小于6小时

问 10:43:59
我的想法是这样子滴~10:46:00
你又回到前面问题里面了,按你现在这个单token, 我举个例子  我3小时登录,9小时候操作一次 你是不是又要我登录,但是我3小时登录(3小时候的7个小时内 我都算活跃的啊)

答 10:46:17
你单token在怎么延长都会有这个问题

答 10:46:56
当然你单token无限长,不会出现过期,但是token已经失去意义

问 10:51:33
比如我0:00登陆,3:00操作一次,这个时候token有效期 7 - 3 小于 6,会返回新的 token,后继客户端使用新的token来登陆,有效期为3:00~100010:52:52
你要想单token  那么每次请求 都返回新的token吧

答 10:53:07
服务器端增加一些压力而已

问 10:53:20
大概是这个意思,嗯嗯
复制代码

 

代码部分

 生成 access_token 和 refresh_token

复制代码
// 生成 access_token 和 refresh_token
func GenToken2(username, password string) (aToken, rToken string, err error) {
    calims := MyCalims{
        Username: username,
        Password: password,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
            Issuer:    "my-project",                               // 签发人
        },
    }
    // 使用指定的签名方法创建签名对象
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, calims)
    // 生成 aToken
    aToken, err = token.SignedString(MySecret)

    // rToken 不需要存储任何自定义数据
    rToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
        ExpiresAt: time.Now().Add(FokenExpireDuration).Unix(), // 过期时间
        Issuer:    "my-project",                               // 签发人
    }).SignedString(MySecret)
    return aToken, rToken, nil
}
View Code
复制代码

 

解析 access_token 

复制代码
// 解析 access_token
func ParasToken2(access_token string) (claims *MyCalims, err error) {
    var token *jwt.Token
    claims = new(MyCalims)
    token, err = jwt.ParseWithClaims(access_token, claims, keyFunc)
    if err != nil {
        return nil, err
    }
    if !token.Valid { // token 是否有效
        err = errors.New("invalid token")
    }
    return claims, nil
}
View Code
复制代码

 

 刷新 token 

复制代码
// 第一步 : 判断 rToken 格式对的,没有过期的
// 第二步 : 判断 aToken 格式对的,但是是过期的
// 第三步 : 生成双 token
func RefreshToken(aToken, rToken string) (newToken, newrToken string, err error) {
    // 第一步 : 判断 rToken 格式对的,没有过期的
    if _, err := jwt.Parse(rToken, keyFunc); err != nil {
        return "", "", err
    }

    // todo 第二步:从旧的 aToken 中解析出 cliams 数据   过期了还能解析出来吗
    var claims MyCalims
    _, err = jwt.ParseWithClaims(aToken, &claims, keyFunc)
    v, _ := err.(*jwt.ValidationError)

    // 当 access token 是过期错误,并且 refresh token 没有过期就创建一个新的 access token
    if v.Errors == jwt.ValidationErrorExpired {
        return GenToken2(claims.Username, claims.Password)
    }
    return "", "", err
}
复制代码

 

中间键 

复制代码
// 中间件 jwt
func JWTAuthMiddleware() func(c *gin.Context) {
    return func(c *gin.Context) {
        // 假设 token 的是在 url 中存放的
        atoken := c.DefaultQuery("atoken", "")
        ftoken := c.DefaultQuery("ftoken", "")
        if atoken == "" || ftoken == "" {
            c.JSON(200, gin.H{
                "msg":  "没有携带 atoken 或者 ftoken",
                "code": 400,
            })
            c.Abort()
            return
        }
        parasToken, err := jwt.ParasToken2(atoken) // 解析 access_token
        if err == nil {                            // 当前的 access_token 格式对,没有过期
            c.JSON(200, gin.H{
                "msg":  "atoken 和 ftoken 没有过期",
                "data": parasToken,
                "code": 400,
            })
            c.Next()
            return
        }
        atoken, rToken, err := jwt.RefreshToken(atoken, ftoken)
        if err != nil {
            c.JSON(200, gin.H{
                "msg":  "您需要重新登录",
                "code": 400,
            })
            c.Abort()
            return
        } else {
            c.JSON(200, gin.H{
                "msg":    "atoken 和 ftoken 没有过期",
                "atoken": atoken,
                "rToken": rToken,
                "code":   400,
            })
            c.Next()
            return
        }
    }
}
View Code
复制代码

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @   dogRuning  阅读(2366)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示