JWT(JSON Web Token)自动延长到期时间

mar*_*ryo 465 security authentication api node.js jwt

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

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

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

但同样,要实现这一点(没有Auth0),我需要存储刷新令牌并保持其过期.那么真正的好处是什么?为什么不只有一个令牌(不是JWT)并在服务器上保持过期?

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

Jos*_*llo 548

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

这一切都取决于应用程序的类型,这是我们推荐的方法.

Web应用程序

一个好的模式是在令牌过期之前刷新令牌.

将令牌过期设置为一周,并在每次用户打开Web应用程序时每隔一小时刷新令牌.如果用户未打开应用程序超过一周,则必须再次登录,这是可接受的Web应用程序UX.

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

移动/本机应用程序

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

我们的想法是刷新令牌永不过期,并且可以始终为有效的JWT进行交换.

永不过期的令牌问题绝不是永远不会.如果丢失手机怎么办?因此,它需要以某种方式由用户识别,并且应用程序需要提供撤销访问的方法.我们决定使用该设备的名称,例如"maryo的iPad".然后,用户可以转到该应用程序并撤消对"maryo的iPad"的访问权限.

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

我们认为JWT对这些用例没用,所以我们使用随机生成的字符串,然后将它存储在我们这边.

  • 对于Web应用程序推荐的方法,如果令牌有效一周,我们不关心有人拦截令牌然后能够使用它这么长时间吗?免责声明:我不太清楚我在说什么. (33认同)
  • @wbeange是拦截是一个问题,即使是使用cookie.你应该使用https. (25认同)
  • @JoséF.Romaniello在您的Web应用程序示例中,除了必须存储令牌之外,一切对我都有意义.我认为JWT的优点是无状态身份验证 - 这意味着Web应用程序不必在签名时存储令牌.我认为服务器可以只检查令牌的有效性,确保它在有效期内,然后发出更新的JWT令牌.你能详细说明一下吗?也许我还不够了解JWT. (12认同)
  • 两个问题/疑虑:1- Web应用案例:为什么不能过期令牌被允许刷新?假设我们设置了短暂的到期时间(1小时),并在令牌过期时对后端服务器进行续订,如您所说.2-在令牌中存储散列(带有随机盐)密码是否存在安全问题?这个想法是,如果它在那里,后端服务器可以在被要求续订时检查数据库中存储的密码,如果密码不匹配则拒绝请求.这将涵盖Mobile/Native应用程序密码更改,允许将解决方案扩展到Mobile用例. (7认同)
  • @DevFox我的意思是刷新令牌不是JWT而是不透明令牌. (5认同)
  • @JoséF.Romaniello,当你讨论生成一个随机字符串并存储它.你能详细说说吗?你的意思是你存储在移动设备上,还是存储在服务器上?如果他们获得了设备名称或者这个随机字符串(假设它在客户端上)会发生什么呢?这不会让他们绕过被撤销的任何令牌吗? (5认同)
  • @ user1870400对于这种混淆感到抱歉,我的意思是浏览器中的客户端,如本地存储或cookie. (5认同)
  • -1暴露公共API,盲目地重新签署任何令牌以延长其验证期是不好的.现在你的所有令牌都有效无限到期.签署令牌的行为应包括在签署时对该令牌中的每一个索赔进行适当的身份验证. (5认同)
  • @JoséF.Romaniello你能说清楚你的意思'我们认为JWT对这些用例没用,所以我们使用随机生成的字符串,然后将它存储在我们这边.哪个用例和什么是随机字符串? (4认同)
  • @JoséF.Romaniello我和Lo-Tan有同样的问题.如果我们在数据库中存储令牌,那么它与传统的Web会话有何不同? (3认同)
  • @psamaan 您的评论意识到了被盗设备的问题,类似于密码更改时的逻辑。我相信如果您已经在服务器端保留了刷新令牌,为了在需要刷新令牌时进行验证,则无需在令牌中包含散列密码,因为您只需要删除所有以前的刷新令牌他更改后的用户是密码。此外,您还可以获得后端的好处,即用户可以根据刷新令牌检查所有打开的会话,并且可以使会话无效。像facebook一样,gmail也一样 (3认同)
  • 我使用了一个角度拦截器来实现每个 http 请求的刷新。 (2认同)
  • **Web 应用程序**:如果您不喜欢每小时刷新一次或者您想尝试其他解决方案,您可以让所有 API 请求通过服务器端代理,该代理以持久方式存储用户的刷新令牌和在用户注销之前拦截 401 响应并交换令牌。请注意,通过这种方式刷新令牌不会暴露给客户端,并且用户将体验与本机应用程序相同的身份验证 (2认同)
  • @le0diaz 我应该澄清一下,我正在尝试找到一个解决方案,即我不在后端保留刷新令牌并完全依赖客户端 JWT,原因是我试图允许后端水平扩展无需维护公共缓存(即完全分布式系统) (2认同)
  • 根据我的经验,每次用户打开 Web 应用程序时都刷新令牌并不是一个好习惯。使用应用程序的一个选项卡会很好,但是当您使用几个选项卡或几个浏览器时,这将是一个问题并导致令牌冲突并断开您使用的所有其他应用程序窗口。 (2认同)
  • 由于刷新令牌的存在,访问令牌的过期日期将变得毫无用处,因为它总是会被刷新? (2认同)

Ian*_*anB 63

在您自己处理auth的情况下(即不使用Auth0之类的提供程序),以下内容可能有效:

  1. 发出JWT令牌,到期时间相对较短,比如15分钟.
  2. 应用程序在任何需要令牌的交易(令牌包含到期日期)之前检查令牌到期日期.如果令牌已过期,则它首先要求API"刷新"令牌(这对UX是透明的).
  3. API获取令牌刷新请求,但首先检查用户数据库以查看是否已针对该用户配置文件设置了"reauth"标志(令牌可以包含用户ID).如果该标志存在,则拒绝令牌刷新,否则发出新令牌.
  4. 重复.

例如,当用户重置密码时,将设置数据库后端中的"reauth"标志.用户下次登录时会删除该标志.

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

  • 而不是在数据库中有另一个字段reauth标志,您可以在令牌中包含hash(bcrypt_password_hash).然后在刷新令牌时,您只需确认hash(bcrypt_password_hash)是否等于令牌中的值.为了拒绝令牌刷新,必须只更新密码哈希. (26认同)
  • 我认为这不安全.如果我是攻击者并偷走你的令牌并将其发送到服务器,服务器将检查并看到该标志设置为true,这很好,因为它会阻止刷新.我认为问题是如果受害者更改了密码,则标志将被设置为false,现在攻击者可以使用该原始令牌进行刷新. (7认同)
  • @ user2924127没有auth解决方案是完美的,总会有权衡.如果攻击者能够"窃取您的令牌",那么您可能需要担心更多问题.设置最大令牌生存期将是对上述内容的有用调整. (5认同)
  • 我认为user2924127的第一条评论实际上是错误的.更改密码后,该帐户被标记为需要重新验证,因此任何现有的过期令牌都将无效. (5认同)
  • @bas,考虑到优化和性能,我认为密码哈希验证将是多余的,并且具有更多的服务器含义.增加令牌的大小,以便签名公司/验证需要更多时间.服务器的其他哈希计算密码.使用额外的字段方法,您只需使用简单的布尔值重新计算.对于额外字段,Db更新的频率较低,但更频繁的令牌刷新.您可以获得任意现有会话(移动,网络等)的强制个人重新登录的可选服务. (4认同)
  • @AndreasLundgren这是真的.请记住,每个令牌都有一个创建时间,因此一种解决方法是简单地将密码重置时间与令牌创建时间进行比较 - 在​​最后一次密码重置之前创建的令牌将无法续订. (4认同)
  • @Ralph 我认为反过来,如果密码被更改,旧的过期令牌仍然有效。_first_ 命中将需要重新验证,但随后的调用将再次通过。因此,现有的过期令牌仍然有效。就像提到的 user2924127 一样。 (2认同)
  • @ user2924127非常有效点!不仅是攻击者受到了侵犯.如果您登录多个设备,最终用户将有一个非常奇怪的行为.密码更新后只能在第一台设备上重新登录...假设您在朋友计算机或互联网咖啡馆,工作学校等的计算机上登录.您可能希望更改密码以锁定帐户你的那些电脑. (2认同)

Bhu*_*ngh 17

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

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

如果您需要更多详细信息,请告诉我,我也可以共享代码(Java + Spring boot)。

  • @BhupinderSingh我认为大多数答案或谷歌结果都会得到与你类似的意见,但我的意见有点不同。刷新令牌,可以帮助使 JWT/无状态访问令牌在短时间内过期,从而使注销工作。但是,如果黑客想要破解您的资源,他们将使用刷新令牌来不断获取新的访问令牌。所以它对安全性并没有真正的帮助。它确实有助于实现传统的注销。 (2认同)

coo*_*ort 13

在后端使用RESTful apis将应用程序移动到HTML5时,我正在修补.我想出的解决方案是:

  1. 成功登录后,将向客户端发出一个会话时间为30分钟(或通常的服务器端会话时间)的令牌.
  2. 创建客户端计时器以调用服务以在令牌到期之前续订令牌.新令牌将替换将来调用中的现有令牌.

如您所见,这减少了频繁的刷新令牌请求.如果用户在触发续订令牌呼叫之前关闭浏览器/应用程序,则先前令牌将及时到期,用户必须重新登录.

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

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

  • 允许客户端请求具有首选到期日期的新令牌对我来说就像是一种安全风险。 (2认同)

Oll*_*ett 13

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

生成新的JWT时,将其编码jwt_version到JWT有效负载中,如果新的JWT应该替换所有其他JWT,则可以选择性地增加该值.

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

  • 嘿,这可能不是一个"问题",取决于你的要求,但你是对的; 这不支持每设备会话管理. (3认同)
  • 这在多个设备上存在问题。基本上,如果您在一台设备上注销,它会在任何地方注销。对? (2认同)
  • 这是否意味着 jwt_version 必须存储在服务器端,以便身份验证方案变得“类似会话”并违背 JWT 的基本目的? (2认同)

LCJ*_*LCJ 8

好问题 - 问题本身就有丰富的信息.

文章Refresh Tokens:何时使用它们以及它们如何与JWT交互为这种情况提供了一个好主意.一些要点是: -

  • 刷新令牌包含获取新访问令牌所需的信息.
  • 刷新令牌也可以过期但是相当长寿.
  • 刷新令牌通常受到严格的存储要求,以确保它们不会泄露.
  • 它们也可以被授权服务器列入黑名单.

另请参阅auth0/angular-jwt angularjs

对于Web API.read 使用ASP .NET Web API 2和Owin在AngularJS App中启用OAuth刷新令牌


cch*_*ain 6

智威汤逊 - 自动刷新

如果您使用的是节点(React/Redux/Universal JS),则可以安装npm i -S jwt-autorefresh.

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

完整的示例实现

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())
Run Code Online (Sandbox Code Playgroud)

免责声明:我是维护者

  • 是的,解码是仅客户端解码,不应该知道这个秘密.该秘密用于对JWT令牌服务器端进行签名,以验证您的签名是否最初用于生成JWT,并且永远不应该从客户端使用.JWT的神奇之处在于它的有效负载可以在客户端进行解码,并且内部声明可用于构建您的UI而无需保密.唯一`jwt-autorefresh`解码它是为了提取`exp`声明,以便它可以确定安排下一次刷新的距离. (3认同)

Byt*_*ter 6

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

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

如果短令牌已过期,但仍然是可信的并且长令牌有效且可信,则它将使用长令牌进行身份验证的服务上的特殊端点刷新短令牌(这是唯一可以使用的).然后它将使用短令牌获取新的长令牌,从而每次刷新短令牌时将其延长一周.

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

延迟编辑:重新阅读这几个月后,我应该指出你可以在刷新短令牌时撤销访问权限,因为它提供了更昂贵的调用的机会(例如,调用数据库以查看用户是否已被禁止),无需在每次拨打您的服务时付费.


Jam*_*s A 6

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

softexp - I set this to 5 mins (300 seconds)
Run Code Online (Sandbox Code Playgroud)

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

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

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

(Math.floor(Date.now() / 1000) > decoded.softexp)
Run Code Online (Sandbox Code Playgroud)

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

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

jwt.verify(token, secret, (err, decoded) => {})
Run Code Online (Sandbox Code Playgroud)

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


小智 6

如今,很多人选择做与JWTs会话管理没有意识到他们正在放弃了的缘故感觉简单。我的回答详细说明了问题的第二部分:

那么真正的好处是什么呢?为什么不只有一个令牌(不是 JWT)并将过期时间保留在服务器上?

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

JWT 能够支持基本的会话管理,但有一些限制。作为自描述令牌,它们不需要服务器端的任何状态。这使它们具有吸引力。例如,如果服务没有持久层,它就不需要仅仅为了会话管理而引入持久层。

然而,无国籍状态也是造成他们缺点的主要原因。由于它们仅以固定内容和到期时间发布一次,因此您无法使用典型的会话管理设置做您想做的事情。

也就是说,您不能按需使它们无效。这意味着您无法实现安全注销,因为无法使已发布的令牌过期。出于同样的原因,您也不能实现空闲超时。一种解决方案是保留黑名单,但这会引入状态。

我写了一篇文章更详细地解释了这些缺点。需要明确的是,您可以通过添加更多复杂性(滑动会话、刷新令牌等)来解决这些问题。

至于其他选项,如果您的客户端仅通过浏览器与您的服务交互,我强烈建议使用基于 cookie 的会话管理解决方案。我还整理了一份目前网络上广泛使用的认证方式列表

  • 感谢链接的优秀简单身份验证指南 / 以及创作它:) 使用 JWT+Cookie 的组合(将 accessToken 保存到 cookie)是一个好的解决方案吗? (2认同)