ChatGPT解决这个技术问题 Extra ChatGPT

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

我想为我们的新 REST API 实现基于 JWT 的身份验证。但是既然在token中设置了过期时间,是不是可以自动延长呢?如果用户在此期间积极使用该应用程序,我不希望用户在每 X 分钟后登录一次。那将是一个巨大的用户体验失败。

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

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

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

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

实际上,一次使用多个有效令牌可能没有安全问题......实际上有无限数量的有效令牌......那么,为什么要有刷新令牌呢?我会在每次请求后重新生成它们,这实际上应该不是问题。
@maryo 我认为在任何给定时间(可能)有数百或数千个未使用的有效 JWT 会增加您的攻击足迹并且存在安全风险。在我看来,JWT 应该谨慎发行,因为它们是以某种方式带有城堡钥匙的访问令牌。

P
Pang

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

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

网络应用

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

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

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

移动/本地应用程序

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

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

永不过期的令牌的问题是从不意味着永远。手机丢了怎么办?因此,用户需要以某种方式识别它,并且应用程序需要提供一种撤销访问权限的方法。我们决定使用设备的名称,例如“maryo's iPad”。然后用户可以转到应用程序并撤销对“maryo 的 iPad”的访问权限。

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

我们认为 JWT 对这些用例没有用处,因此我们使用随机生成的字符串并将其存储在我们这边。


对于 Web 应用程序推荐的方法,如果令牌有效期为一周,我们不担心有人截获令牌然后能够使用这么长时间吗?免责声明:我不太清楚我在说什么。
@wbeange 是的,即使使用 cookie,拦截也是一个问题。您应该使用 https。
@JoséF.Romaniello 在您的 Web 应用程序示例中,除了必须存储令牌之外,一切对我来说都很有意义。我认为 JWT 的美妙之处在于无状态身份验证——这意味着 Web 应用程序不必在签名时存储令牌。我认为服务器可以只检查令牌的有效性,确保它在有效期内,然后发出更新的 JWT 令牌。你能详细说明一下吗?也许我只是还不够了解 JWT。
@user1870400 抱歉,我的意思是浏览器中的客户端,例如本地存储或 cookie。
-1 公开一个盲目地重新签署任何令牌以延长其验证期的公共 API 是不好的。现在你所有的代币都有一个有效的无限期。签署令牌的行为应包括在签署时对该令牌中提出的每个声明进行适当的身份验证检查。
S
Simon East

在您自己处理身份验证的情况下(即不使用像 Auth0 这样的提供程序),以下可能有效:

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

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

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


我不认为这会是安全的。如果我是攻击者并窃取了您的令牌并将其发送到服务器,服务器将检查并看到该标志设置为 true,这很好,因为它会阻止刷新。我认为的问题是,如果受害者更改了密码,该标志将设置为 false,现在攻击者可以使用该原始令牌进行刷新。
@user2924127 没有任何身份验证解决方案是完美的,总会有权衡。如果攻击者能够“窃取您的令牌”,那么您可能需要担心更大的问题。设置最大令牌生命周期将是对上述内容的有用调整。
您可以在令牌中包含 hash(bcrypt_password_hash) ,而不是在数据库中包含另一个字段 reauth 标志。然后在刷新令牌时,您只需确认 hash(bcrypt_password_hash) 是否等于令牌中的值。为了拒绝令牌刷新,只需更新密码哈希。
@bas,考虑到优化和性能,我认为密码哈希验证将是多余的并且对服务器有更多影响。增加令牌的大小,以便签名公司/验证需要更多时间。用于密码的服务器的附加哈希计算。使用额外字段方法,您只需使用简单的布尔值在重新计算中验证。额外字段的数据库更新频率较低,但令牌刷新频率更高。并且您可以获得强制个人重新登录任何现有会话(移动、网络等)的可选服务。
我认为 user2924127 的第一条评论实际上是错误的。更改密码时,该帐户被标记为需要重新认证,因此任何现有的过期令牌都将无效。
B
Bhupinder Singh

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

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

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


如果你在 GitHub 上可以分享你的项目链接吗?
嗨@BhupinderSingh。我的问题是为什么你为访问令牌设置了更少的过期时间?!您可以设置刷新令牌的长时间到期来访问令牌并且您根本没有使用刷新令牌。我对吗?
@BhupinderSingh 我认为大多数答案或谷歌结果都与您有相似的意见,但我的意见有点不同。刷新令牌,可以帮助 JWT/无状态访问令牌在短时间内过期,从而使注销工作。但是,如果黑客想要破解您的资源,他们将使用刷新令牌来不断获取新的访问令牌。因此,它对安全性并没有真正的帮助。它确实有助于实现传统的注销。
令牌的 LocalStorage 不安全
t
t7tran

在将我们的应用程序迁移到 HTML5 并在后端使用 RESTful api 时,我一直在修补。我想出的解决方案是:

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

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

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

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


如果计算机被挂起/睡眠怎么办。计时器仍然会计数直到到期,但令牌实际上已经到期。定时器在这种情况下不起作用
@AlexParij您可以与固定时间进行比较,如下所示:stackoverflow.com/a/35182296/1038456
允许客户端请求具有首选到期日期的新令牌对我来说闻起来像是一种安全风险。
O
Ollie Bennett

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

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

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


这有多个设备的问题。本质上,如果您在一台设备上注销,它会在任何地方注销。正确的?
嘿,根据您的要求,这可能不是“问题”,但您是对的;这不支持每设备会话管理。
这是否意味着 jwt_version 必须存储在服务器端,以使身份验证方案变得“类似会话”并违背 JWT 的基本目的?
通过 devise-jwt 自述文件,有更多关于撤销 JWT here 的讨论和一些其他选项。
此外,jwt_version 概念最好通过(保留名称)jti 或“JWT ID”声明指定为唯一代码 - 请参阅 specification
C
Community

今天,很多人选择使用 JWT 进行会话管理,却没有意识到他们为了简单而放弃了什么。我的回答详细说明了问题的第二部分:

那么真正的好处是什么?为什么不只有一个令牌(不是 JWT)并在服务器上保留过期时间?还有其他选择吗?使用 JWT 不适合这种情况吗?

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

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

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

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

至于其他选项,如果您的客户仅通过浏览器与您的服务交互,我强烈建议使用基于 cookie 的会话管理解决方案。我还compiled a list authentication methods目前在网络上广泛使用。


感谢链接/和创作它的优秀简单的身份验证指南:) 使用 JWT+Cookies 的组合(将 accessToken 保存到 cookie)会是一个好的解决方案吗?
将 JWT 保存到 cookie 效果很好。它将为您的 cookie 值完整性保护,但如果您需要支持更高级的场景(如空闲超时),您仍然需要一些方法来按需将令牌列入黑名单。我会在 cookie 中选择一个简单的会话 ID。
C
Community

jwt-自动刷新

如果您使用的是节点(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())

免责声明:我是维护者


关于这个问题,我看到了它使用的解码功能。它是否假设可以在不使用秘密的情况下对 JWT 进行解码?它是否适用于使用秘密签名的 JWT?
是的,解码是仅限客户端的解码,不应该知道秘密。该密钥用于在服务器端对 JWT 令牌进行签名,以验证您的签名是否最初用于生成 JWT,并且永远不应从客户端使用。 JWT 的神奇之处在于它的有效负载可以在客户端进行解码,并且可以使用里面的声明来构建您的 UI,而无需保密。 jwt-autorefresh 对其进行解码的唯一目的是提取 exp 声明,以便它可以确定计划下一次刷新的时间。
哦,很高兴知道,有些事情没有意义,但现在它有了。感谢你的回答。
L
LCJ

好问题 - 问题本身包含丰富的信息。

文章 Refresh Tokens: When to Use Them and How They Interact with JWTs 为这种情况提供了一个好主意。一些要点是:-

刷新令牌携带获取新访问令牌所需的信息。

刷新令牌也可以过期,但寿命相当长。

刷新令牌通常受到严格的存储要求,以确保它们不会泄露。

它们也可以被授权服务器列入黑名单。

另请查看 auth0/angular-jwt angularjs

对于 Web API。阅读Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin


也许我读错了...但是标题以“刷新令牌...”开头的文章不包含任何有关刷新令牌的内容,除了您在此处提到的内容。
B
BytePorter

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

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

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

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

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


J
James A

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

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) => {})

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


这个策略看起来不错。但我认为应该补充一种“最大更新量”,因为(也许)用户会话可以永远活着。
您可以在令牌数据中设置一个 hardExp 变量来设置一个最大日期以强制使令牌过期,或者可以设置一个计数器,该计数器在令牌更新时递减,从而限制令牌更新总量。
s
sjaiswal

这种方法怎么样:

对于每个客户端请求,服务器将令牌的过期时间与 (currentTime - lastAccessTime) 进行比较

如果 expireTime < (currentTime - lastAccessedTime),它将最后的 lastAccessedTime 更改为 currentTime。

如果浏览器的不活动持续时间超过expirationTime,或者浏览器窗口关闭并且expirationTime > (currentTime - lastAccessedTime),则服务器可以使令牌过期并要求用户再次登录。

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


在这一天它是一个不错的选择吗,它看起来很容易实现。
在这种情况下,您将 lastAccessedTime 存储在哪里?您必须在后端和每个请求上执行此操作,因此它成为不理想的有状态解决方案。
B
Batman Rises

参考 - Refresh Expired JWT Example

另一种选择是,一旦 JWT 过期,用户/系统将调用另一个 url,假设 /refreshtoken。与此请求一起,过期的 JWT 也应该被传递。然后,服务器将返回一个可供用户/系统使用的新 JWT。

https://i.stack.imgur.com/u0cRi.png


我认为使用这样的过期令牌进行身份验证没有多大意义。令牌可能会泄漏,因此更安全的做法是在过期之前使用仍然有效的 JWT 定期更新。
如果您在 JWT 过期之前定期刷新它,那么当用户有多个未过期的令牌时会发生什么?
J
Joseph Ditton

我知道这是一个老问题,但我使用会话和令牌身份验证的混合。我的应用程序是微服务的组合,因此我需要使用基于令牌的身份验证,这样每个微服务都不需要访问集中式数据库进行身份验证。我向我的用户发出 2 个 JWT(由不同的秘密签名):

标准 JWT,用于对请求进行身份验证。此令牌将在 15 分钟后过期。用作放置在安全 cookie 中的刷新令牌的 JWT。只有一个端点(实际上是它自己的微服务)接受这个令牌,它是 JWT 刷新端点。它必须伴随着帖子正文中的 CSRF 令牌,以防止该端点上的 CRSF。 JWT 刷新端点将会话存储在数据库中(会话的 id 和用户被编码到刷新 JWT 中)。这允许用户或管理员使刷新令牌无效,因为令牌必须同时验证并匹配该用户的会话。

这工作得很好,但比仅使用基于会话的身份验证与 cookie 和 CSRF 令牌要复杂得多。因此,如果您没有微服务,那么基于会话的身份验证可能是要走的路。


I
Ido Bleicher

如果您使用的是 AWS Amplify 和 Cognito,这将为您带来神奇的效果:

使用 Auth.currentSession() 获取当前的有效令牌,如果当前已过期,则获取新令牌。 Amplify 将处理它作为后备,使用一些间隔作业以每 x 分钟(可能 10 分钟)按需刷新令牌。如果您有一个长时间运行的过程,例如上传一个需要一个多小时的非常大的视频(可能是由于网络速度较慢),那么您的令牌将在上传期间过期并且 amplify 不会为您自动更新。在这种情况下,此策略将起作用。定期更新您的令牌。文档中没有提到如何按需刷新,所以在这里。

import { Auth } from 'aws-amplify';

try {
  const cognitoUser = await Auth.currentAuthenticatedUser();
  const currentSession = await Auth.currentSession();
  cognitoUser.refreshSession(currentSession.refreshToken, (err, session) => {
    console.log('session', err, session);
    const { idToken, refreshToken, accessToken } = session;
    // do whatever you want to do now :)
  });
} catch (e) {
  console.log('Unable to refresh Token', e);
}

来源:https://github.com/aws-amplify/amplify-js/issues/2560


H
Hatrox New

services.Configure(Configuration.GetSection("ApplicationSettings"));

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 

        services.AddDbContext<AuthenticationContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));

        services.AddDefaultIdentity<ApplicationUser>()
            .AddEntityFrameworkStores<AuthenticationContext>();

        services.Configure<IdentityOptions>(options =>
        {
            options.Password.RequireDigit = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireLowercase = false;
            options.Password.RequireUppercase = false;
            options.Password.RequiredLength = 4;
        }
        );

        services.AddCors();

        //Jwt Authentication

        var key = Encoding.UTF8.GetBytes(Configuration["ApplicationSettings:JWT_Secret"].ToString());

        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(x=> {
            x.RequireHttpsMetadata = false;
            x.SaveToken = false;
            x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            };
        });
    }

如果您可以添加一些解释会很有帮助