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

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

我发现Auth0是如何解决这个问题的。他们不仅使用JWT令牌,还使用refresh令牌: https://auth0.com/docs/tokens/refresh-tokens

但是,要实现这一点(没有Auth0),我需要存储刷新令牌并维护它们的过期。那么真正的好处是什么呢?为什么不只有一个令牌(不是JWT)并将过期时间保存在服务器上呢?

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


当前回答

jwt-autorefresh

如果你正在使用node (React / Redux / Universal JS),你可以安装npm i -S jwt-autorefresh。

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

完整的示例实现

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())

免责声明:我是维护者

其他回答

这个方法怎么样:

对于每个客户端请求,服务器将令牌的过期时间与(currentTime - lastAccessTime)进行比较。 如果expirationTime < (currentTime - lastAccessedTime),则将上一次lastAccessedTime更改为currentTime。 如果浏览器上的不活动时间超过了过期时间,或者如果浏览器窗口被关闭并且过期时间为> (currentTime - lastAccessedTime),那么服务器可以使令牌过期并要求用户再次登录。

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

jwt-autorefresh

如果你正在使用node (React / Redux / Universal JS),你可以安装npm i -S jwt-autorefresh。

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

完整的示例实现

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())

免责声明:我是维护者

当我在后端将我们的应用程序移动到带有RESTful api的HTML5时,我正在进行修补。我想到的解决办法是:

成功登录后,客户端将获得一个会话时间为30分钟(或通常的服务器端会话时间)的令牌。 创建一个客户端计时器来调用服务,以便在令牌到期之前更新令牌。新的令牌将在未来的调用中取代现有的令牌。

如您所见,这减少了频繁的刷新令牌请求。如果用户在更新令牌调用触发之前关闭浏览器/应用程序,之前的令牌将及时过期,用户将不得不重新登录。

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

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

另一种使jwt失效的解决方案是在用户表上实现一个新的jwt_version整数列,而不需要在后端进行任何额外的安全存储。如果用户希望注销或过期现有令牌,他们只需增加jwt_version字段。

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

在验证JWT时,jwt_version字段将与user_id进行比较,只有匹配时才授予授权。

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

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

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

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

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

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

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

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

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

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