关于node.js:JWT(JSON Web Token)自动延长到期时间

JWT (JSON Web Token) automatic prolongation of expiration

我想对我们的新RESTAPI实现基于JWT的身份验证。但是由于在令牌中设置了过期,是否可以自动延长它?如果用户在这段时间内积极使用该应用程序,我不希望他们每隔X分钟登录一次。那将是一个巨大的用户体验失败。

但是延长到期时间会创建一个新的令牌(旧的令牌在到期之前仍然有效)。在每次请求之后生成一个新的令牌对我来说都是愚蠢的。当多个令牌同时有效时,听起来像是一个安全问题。当然,我可以使用黑名单使旧的旧的失效,但我需要存储令牌。JWT的一个好处是没有存储。

我发现Auth0是如何解决的。它们不仅使用JWT令牌,还使用刷新令牌:https://docs.auth0.com/refresh-token

但是,为了实现这个(没有auth0),我需要存储刷新令牌并保持它们的有效期。那么真正的好处是什么呢?为什么不只有一个令牌(而不是JWT)并在服务器上保持到期?

还有其他选择吗?使用JWT是否不适合这种情况?


我在Auth0工作,参与了刷新令牌功能的设计。

这完全取决于应用程序的类型,这里是我们推荐的方法。

Web应用程序

一个好的模式是在令牌到期之前刷新它。

将令牌过期设置为一周,并在每次用户打开Web应用程序时以及每一小时刷新一次令牌。如果一个用户超过一周没有打开应用程序,他们将不得不再次登录,这是可接受的Web应用程序UX。

要刷新令牌,您的API需要一个新的端点,该端点接收一个有效的、未过期的JWT,并返回带有新到期字段的同一个已签名的JWT。然后Web应用程序将把令牌存储在某个地方。

移动/本机应用程序

大多数本机应用程序只登录一次。

其思想是刷新令牌永远不会过期,并且可以始终将其交换为有效的JWT。

令牌永不过期的问题是它永远意味着永不过期。如果你的手机丢了怎么办?因此,它需要以某种方式被用户识别,应用程序需要提供一种方法来撤销访问。我们决定使用该设备的名称,例如"Maryo的iPad"。然后用户可以进入应用程序并撤销对"Maryo'siPad"的访问。

另一种方法是撤销特定事件的刷新令牌。一个有趣的事件是更改密码。

我们相信JWT对于这些用例没有用处,所以我们使用随机生成的字符串,并将其存储在我们的一侧。


在您自己处理auth的情况下(即不要使用auth0这样的提供者),以下可能有效:

  • 发行有效期相对较短的JWT代币,比如15分钟。
  • 应用程序在任何需要令牌的事务(令牌包含到期日期)之前检查令牌到期日期。如果令牌已过期,那么它首先要求API"刷新"令牌(这是对UX透明的)。
  • API获取令牌刷新请求,但首先检查用户数据库,查看是否已针对该用户配置文件设置了"重新授权"标志(令牌可以包含用户ID)。如果标志存在,则拒绝令牌刷新,否则将发出新令牌。
  • 重复。
  • 例如,当用户重置了密码时,将设置数据库后端中的"重新授权"标志。当用户下次登录时,该标志将被删除。

    另外,假设您有一个策略,即用户必须至少每72小时登录一次。在这种情况下,API令牌刷新逻辑还将检查用户数据库中的最后登录日期,并在此基础上拒绝/允许令牌刷新。


    另一种解决方案是在后端没有任何额外的安全存储的情况下使JWT无效,即在用户表上实现一个新的jwt_version整数列。如果用户希望注销或使现有令牌过期,则只需增加jwt_version字段。

    在生成新的JWT时,将jwt_version编码到JWT有效载荷中,如果新的JWT应该替换所有其他的,可以选择提前增加值。

    在验证JWT时,将jwt_version字段与user_id字段进行比较,并且仅在匹配时授予授权。


    在将我们的应用程序转移到HTML5时,我在后端使用RESTfulAPI进行了一些修改。我提出的解决方案是:

  • 成功登录后,向客户机颁发会话时间为30分钟(或任何通常的服务器端会话时间)的令牌。
  • 创建客户端计时器,以便在令牌到期之前调用服务以续订令牌。新令牌将替换现有的将来调用。
  • 如您所见,这减少了频繁的刷新令牌请求。如果用户在触发续订令牌调用之前关闭浏览器/应用程序,则以前的令牌将及时过期,用户必须重新登录。

    可以实施更复杂的策略来满足用户不活动的需要(例如,忽略了打开的浏览器选项卡)。在这种情况下,续订令牌调用应包括预期的到期时间,该时间不应超过定义的会话时间。应用程序必须相应地跟踪最后一次用户交互。

    我不喜欢设置长过期的想法,因此这种方法可能不能很好地用于需要较少频率的身份验证的本机应用程序。


    好问题——问题本身就包含了丰富的信息。

    本文刷新令牌:何时使用它们以及它们如何与JWT交互为这个场景提供了一个好主意。有些要点是:

    • 刷新令牌携带获取新访问所需的信息令牌。
    • 刷新令牌也可以过期,但使用寿命相当长。
    • 刷新令牌通常受到严格的存储要求确保没有泄漏。
    • 它们也可以被授权服务器列入黑名单。

    还可以看看Auth0/AngularJWT AngularJS

    对于Web API。已阅读"使用ASP.NET Web API 2和OWIN在AngularJS应用程序中启用OAuth刷新令牌"


    实际上,我在PHP中使用Guzzle客户机为API创建了一个客户机库,但是这个概念应该适用于其他平台。

    基本上,我发行两个代币,一个短(5分钟)的,一个长的,一周后到期。如果客户端库接收到对某个请求的401响应,则使用中间件尝试对短令牌进行一次刷新。然后它将再次尝试原始请求,如果它能够刷新,将得到正确的响应,这对用户是透明的。如果失败,它将把401发送给用户。

    如果短令牌已过期,但仍然是可信的,并且长令牌有效且可信,它将使用长令牌认证的服务上的特殊终结点刷新短令牌(这是它唯一可以使用的东西)。然后,它将使用短令牌获取新的长令牌,从而在每次刷新短令牌时将其延长一周。

    这种方法还允许我们在最多5分钟内撤销访问,这对于我们的使用是可以接受的,而不必存储令牌的黑名单。

    后期编辑:在我头脑中重新阅读了这几个月后,我应该指出,当刷新短令牌时,你可以取消访问,因为它提供了一个更昂贵的调用的机会(例如,调用数据库查看用户是否被禁止),而不必在每次调用你的服务时付费。


    JWT自动刷新

    如果您使用的是node(react/redux/universal js),则可以安装npm i -S jwt-autorefresh

    此库在访问令牌到期前(基于令牌中编码的exp声明)以用户计算的秒数调度JWT令牌的刷新。它有一个广泛的测试套件,并检查了相当多的条件,以确保任何奇怪的活动都伴随着有关您的环境配置错误的描述性消息。

    全示例实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import autorefresh from 'jwt-autorefresh'

    /** Events in your app that are triggered when your user becomes authorized or deauthorized. */
    import { onAuthorize, onDeauthorize } from './events'

    /** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
    const refresh = () => {
      const init =  { method: 'POST'
                    , headers: { 'Content-Type': `application/x-www-form-urlencoded` }
                    , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
                    }
      return fetch('/oauth/token', init)
        .then(res => res.json())
        .then(({ token_type, access_token, expires_in, refresh_token }) => {
          localStorage.access_token = access_token
          localStorage.refresh_token = refresh_token
          return access_token
        })
    }

    /** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
    const leadSeconds = () => {
      /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
      const jitter = Math.floor(Math.random() * 30)

      /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
      return 60 + jitter
    }

    let start = autorefresh({ refresh, leadSeconds })
    let cancel = () => {}
    onAuthorize(access_token => {
      cancel()
      cancel = start(access_token)
    })

    onDeauthorize(() => cancel())

    免责声明:我是维护者


    我通过在令牌数据中添加一个变量来解决这个问题:

    1
    softexp - I set this to 5 mins (300 seconds)

    我将expiresIn选项设置为我想要的时间,然后用户将被迫再次登录。我的设定是30分钟。这必须大于softexp的值。

    当我的客户端应用程序向服务器API发送请求(需要令牌的地方,如客户列表页)时,服务器根据其原始到期(expiresIn值)检查提交的令牌是否仍然有效。如果它无效,服务器将以特定于此错误的状态响应,例如INVALID_TOKEN

    如果基于expiredIn值的令牌仍然有效,但已经超过了softexp值,服务器将对此错误以单独的状态响应,例如EXPIRED_TOKEN

    1
    (Math.floor(Date.now() / 1000) > decoded.softexp)

    在客户端,如果它收到EXPIRED_TOKEN响应,它应该通过向服务器发送续订请求来自动续订令牌。这对用户是透明的,并自动处理客户端应用程序。

    服务器中的续订方法必须检查令牌是否仍然有效:

    1
    jwt.verify(token, secret, (err, decoded) => {})

    如果上述方法失败,服务器将拒绝续订令牌。


    这种方法怎么样:

    • 对于每个客户机请求,服务器将令牌的过期时间与(currentTime-lastAccessTime)进行比较。
    • 如果ExpirationTime<(CurrentTime-LastAccessedTime),它会将LastAccessedTime更改为CurrentTime。
    • 如果浏览器处于非活动状态超过expirationtime或浏览器窗口关闭且expirationtime>(currentTime-lastaccessedtime),则服务器可以使令牌过期,并要求用户重新登录。

    在这种情况下,刷新令牌不需要额外的端点。希望有任何反馈。


    以下是撤销JWT访问令牌的步骤:

    1)登录时,发送2个令牌(访问令牌、刷新令牌)响应客户端。2)访问令牌的到期时间较短,刷新的到期时间较长。3)客户端(前端)将刷新令牌存储在本地存储器中,访问令牌存储在cookie中。4)客户端将使用访问令牌调用API。但到期后,从本地存储中选择刷新令牌并调用auth server api以获取新令牌。5)您的认证服务器将公开一个API,它将接受刷新令牌并检查其有效性,并返回一个新的访问令牌。6)一旦刷新令牌过期,用户将被注销。

    请让我知道,如果你需要更多的细节,我可以共享代码(Java+Spring Bug)。