JWT(JSON Web Token)自动延长过期时间
Posted
技术标签:
【中文标题】JWT(JSON Web Token)自动延长过期时间【英文标题】:JWT (JSON Web Token) automatic prolongation of expiration 【发布时间】:2014-12-31 13:56:00 【问题描述】:我想对我们的新 REST API 实施基于 JWT 的身份验证。但是既然在token中设置了过期时间,是不是可以自动延长呢?如果用户在此期间积极使用该应用程序,我不希望用户在每 X 分钟后登录一次。那将是一个巨大的用户体验失败。
但是延长到期时间会创建一个新令牌(旧令牌在到期之前仍然有效)。在每个请求之后生成一个新令牌对我来说听起来很愚蠢。当多个令牌同时有效时,这听起来像是一个安全问题。当然,我可以使用黑名单使旧的使用无效,但我需要存储令牌。 JWT 的好处之一是无需存储。
我发现了 Auth0 是如何解决它的。他们不仅使用 JWT 令牌,还使用刷新令牌: https://auth0.com/docs/tokens/refresh-tokens
但同样,为了实现这一点(没有 Auth0),我需要存储刷新令牌并保持它们的过期时间。那么真正的好处是什么?为什么不只有一个令牌(不是 JWT)并在服务器上保留过期时间?
还有其他选择吗?使用 JWT 不适合这种情况吗?
【问题讨论】:
实际上一次使用多个有效令牌可能没有安全问题...实际上有效令牌的数量是无限的...那么,为什么要有刷新令牌呢?我会在每次请求后重新生成它们,这实际上应该不是问题。 @maryo 我认为在任何给定时间都有(可能)成百上千个未使用的有效 JWT 会增加您的攻击足迹并且存在安全风险。在我看来,JWT 应该谨慎发行,因为它们是以某种方式带有城堡钥匙的访问令牌。 【参考方案1】:我在 Auth0 工作,参与了刷新令牌功能的设计。
这完全取决于应用程序的类型,这是我们推荐的方法。
网络应用程序
一个好的模式是在令牌过期之前刷新它。
将令牌有效期设置为一周,并在用户每次打开 Web 应用程序时和每隔一小时刷新一次令牌。如果用户超过一周未打开应用程序,他们将不得不再次登录,这是可接受的 Web 应用程序 UX。
要刷新令牌,您的 API 需要一个新的端点来接收有效的、未过期的 JWT,并返回具有新过期字段的相同签名 JWT。然后 Web 应用程序会将令牌存储在某处。
移动/原生应用程序
大多数本地应用程序只登录一次。
这个想法是刷新令牌永不过期,并且可以始终交换为有效的 JWT。
永不过期令牌的问题在于,never 意味着永不。手机丢了怎么办?因此,用户需要以某种方式识别它,并且应用程序需要提供一种撤销访问权限的方法。我们决定使用设备的名称,例如“玛丽的 iPad”。然后用户可以转到应用程序并撤销对“玛丽的 iPad”的访问权限。
另一种方法是撤销特定事件的刷新令牌。一个有趣的事件是更改密码。
我们认为 JWT 对这些用例没有用处,因此我们使用随机生成的字符串并将其存储在我们这边。
【讨论】:
对于web应用推荐的方式,如果token有效期为一周,我们不担心有人截取token然后可以使用这么长时间吗?免责声明:我不太清楚我在说什么。 @wbeange 是的,即使使用 cookie,拦截也是一个问题。你应该使用 https。 @JoséF.Romaniello 在您的 Web 应用程序示例中,除了必须存储令牌之外,一切对我来说都很有意义。我认为 JWT 的美妙之处在于无状态身份验证——这意味着 Web 应用程序不必在签名时存储令牌。我认为服务器可以检查令牌的有效性,确保它在有效期内,然后发出更新的 JWT 令牌。你能详细说明一下吗?也许我只是对 JWT 还不够了解。 @user1870400 很抱歉造成混淆,我指的是浏览器中的客户端,例如本地存储或 cookie。 -1 公开一个盲目地重新签署任何令牌以延长其验证期的公共 API 是不好的。现在你所有的代币都有一个有效的无限期。签署令牌的行为应包括在签署时对该令牌中提出的每项声明进行适当的身份验证检查。【参考方案2】:如果您自己处理身份验证(即不使用像 Auth0 这样的提供程序),以下可能会起作用:
-
发行有效期相对较短的 JWT 令牌,例如 15 分钟。
应用程序在任何需要令牌的交易之前检查令牌到期日期(令牌包含到期日期)。如果令牌已过期,则它首先要求 API“刷新”令牌(这对 UX 透明)。
API 获取令牌刷新请求,但首先检查用户数据库以查看是否针对该用户配置文件设置了“reauth”标志(令牌可以包含用户 ID)。如果存在该标志,则拒绝刷新令牌,否则会发出新令牌。
重复。
例如,当用户重置密码时,将设置数据库后端中的“reauth”标志。当用户下次登录时,该标志将被删除。
此外,假设您有一项政策,即用户必须每 72 小时至少登录一次。在这种情况下,您的 API 令牌刷新逻辑还将检查用户数据库中用户的最后登录日期,并在此基础上拒绝/允许令牌刷新。
【讨论】:
我认为这不安全。如果我是攻击者并窃取了您的令牌并将其发送到服务器,服务器将检查并看到该标志设置为 true,这很好,因为它会阻止刷新。我认为的问题是,如果受害者更改了密码,该标志将设置为 false,现在攻击者可以使用该原始令牌进行刷新。 @user2924127 没有任何身份验证解决方案是完美的,总会有权衡。如果攻击者能够“窃取您的令牌”,那么您可能需要担心更大的问题。设置最大令牌生命周期将是对上述内容的有用调整。 您可以在令牌中包含哈希(bcrypt_password_hash),而不是在数据库中包含另一个字段 reauth 标志。然后在刷新令牌时,您只需确认 hash(bcrypt_password_hash) 是否等于令牌中的值。为了拒绝令牌刷新,只需更新密码哈希即可。 @bas,考虑到优化和性能,我认为密码哈希验证将是多余的并且对服务器有更多影响。增加令牌的大小,以便签名公司/验证需要更多时间。用于密码的服务器的附加哈希计算。使用额外字段方法,您只需使用简单的布尔值在重新计算中验证。额外字段的数据库更新频率较低,但令牌刷新频率更高。并且您可以获得强制个人重新登录任何现有会话(移动、网络等)的可选服务。 我认为 user2924127 的第一条评论实际上是错误的。更改密码时,该帐户被标记为需要重新认证,因此任何现有的过期令牌都将无效。【参考方案3】:我在将我们的应用程序迁移到 html5 并在后端使用 RESTful api 时进行了修补。我想出的解决方案是:
-
成功登录后,客户端会收到一个会话时间为 30 分钟(或任何通常的服务器端会话时间)的令牌。
创建客户端计时器以调用服务以在令牌到期之前更新令牌。新令牌将替换未来调用中的现有令牌。
如您所见,这减少了频繁的刷新令牌请求。如果用户在触发更新令牌调用之前关闭浏览器/应用程序,则之前的令牌将及时过期,用户将不得不重新登录。
可以实施更复杂的策略来满足用户不活动的需求(例如,忽略打开的浏览器选项卡)。在这种情况下,更新令牌调用应包括不应超过定义的会话时间的预期到期时间。应用程序必须相应地跟踪最后的用户交互。
我不喜欢设置长过期时间的想法,因此这种方法可能不适用于需要较少身份验证的本机应用程序。
【讨论】:
如果计算机被挂起/睡眠怎么办。计时器仍然会计数直到到期,但令牌实际上已经到期。定时器在这种情况下不起作用 @AlexParij 您将与固定时间进行比较,例如:***.com/a/35182296/1038456 允许客户端请求具有首选到期日期的新令牌对我来说是一种安全风险。【参考方案4】:以下是撤销 JWT 访问令牌的步骤:
1) 当您登录时,发送 2 个令牌(访问令牌、刷新令牌)以响应客户端。 2) 访问令牌的过期时间较短,刷新的过期时间较长。 3) 客户端(前端)将刷新令牌存储在其本地存储中,并将访问令牌存储在 cookie 中。 4)客户端将使用访问令牌调用api。但是当它过期时,从本地存储中选择刷新令牌并调用身份验证服务器 api 以获取新令牌。 5) 您的身份验证服务器将公开一个 API,该 API 将接受刷新令牌并检查其有效性并返回一个新的访问令牌。 6) 一旦刷新令牌过期,用户将被注销。
如果您需要更多详细信息,请告诉我,我也可以分享代码(Java + Spring boot)。
【讨论】:
如果你在 GitHub 上有你的项目链接,可以分享一下吗? click here for the link 嗨@BhupinderSingh。我的问题是为什么你为访问令牌设置了更少的过期时间?!您可以设置刷新令牌的长时间到期来访问令牌并且您根本没有使用刷新令牌。我说的对吗? @AliSohrabi :访问令牌需要较短的到期时间,以便您强制应用程序不断刷新它们。您为服务提供了撤销应用程序访问权限的机会。它只是更保护您的 api。如果有人拥有访问令牌,则在短时间内它将过期,因此他还需要拥有刷新令牌才能获得新的访问权限。它只会增加安全的机会。您对应用程序的访问方式越复杂,您获得的安全性就越高。 @BhupinderSingh 我认为大多数答案或谷歌结果都与您有相似的意见,但我的意见有点不同。刷新令牌,可以帮助 JWT/无状态访问令牌在短时间内过期,从而使注销工作。但是,如果黑客想要破解您的资源,他们将使用刷新令牌来不断获取新的访问令牌。因此,它对安全性并没有真正的帮助。它确实有助于实现传统的注销。【参考方案5】:另一种使 JWT 失效的解决方案是在用户表上实现一个新的 jwt_version
整数列,而无需在后端提供任何额外的安全存储。如果用户希望注销或使现有令牌过期,他们只需增加 jwt_version
字段即可。
在生成新的 JWT 时,将 jwt_version
编码到 JWT 负载中,如果新的 JWT 应该替换所有其他值,则可以选择预先增加值。
在验证 JWT 时,jwt_version
字段与user_id
进行比较,只有匹配时才会授予授权。
【讨论】:
这有多个设备的问题。本质上,如果您在一台设备上注销,它会在任何地方注销。对吗? 嘿,根据您的要求,这可能不是“问题”,但您是对的;这不支持每设备会话管理。 这是否意味着 jwt_version 必须存储在服务器端,这样身份验证方案才会变得“类似会话”并违背 JWT 的基本目的? 通过devise-jwt 自述文件,有更多关于撤销 JWT here 的讨论和其他选项。 此外,jwt_version
概念最好通过(保留名称)jti
或“JWT ID”声明指定为唯一代码 - 请参阅 specification【参考方案6】:
jwt-autorefresh
如果您使用的是节点(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
声明,以便它可以确定计划下一次刷新的时间。
哦,很高兴知道,有些事情没有意义,但现在可以了。感谢您的回答。【参考方案7】:
我实际上使用 Guzzle 客户端在 php 中实现了这一点,为 api 创建了一个客户端库,但这个概念应该适用于其他平台。
基本上,我发行了两个令牌,一个是短的(5 分钟),一个是一周后到期的长的。如果客户端库收到对某个请求的 401 响应,则客户端库使用中间件尝试刷新一次短令牌。然后它会再次尝试原始请求,如果它能够刷新得到正确的响应,对用户透明。如果失败,它只会将 401 发送给用户。
如果短令牌已过期,但仍然是真实的,并且长令牌是有效且真实的,它将使用长令牌认证的服务上的特殊端点刷新短令牌(这是唯一可以使用的东西为了)。然后它将使用短令牌来获取新的长令牌,从而在每次刷新短令牌时将其延长一周。
这种方法还允许我们在最多 5 分钟内撤销访问权限,这对于我们的使用来说是可以接受的,而无需存储令牌黑名单。
后期编辑:在我脑海中浮现这几个月后重新阅读,我应该指出,您可以在刷新短令牌时撤销访问权限,因为它为更昂贵的调用提供了机会(例如调用数据库以查看如果用户已被禁止)而无需为每次调用您的服务付费。
【讨论】:
【参考方案8】:今天,很多人选择使用 JWT 进行会话管理,却没有意识到他们为了感知简单而放弃了什么。我的回答详细说明了问题的第二部分:
那么真正的好处是什么?为什么不只有一个令牌(不是 JWT)并在服务器上保留过期时间?
还有其他选择吗?使用 JWT 不适合这种情况吗?
JWT 能够支持基本会话管理,但有一些限制。作为自描述令牌,它们不需要服务器端的任何状态。这使它们具有吸引力。例如,如果服务没有持久层,它就不需要为了会话管理而引入持久层。
然而,无国籍也是造成他们缺点的主要原因。由于它们仅发布一次,具有固定的内容和过期时间,因此您无法使用典型的会话管理设置来做您想做的事情。
也就是说,您不能按需使它们无效。这意味着您无法实施安全注销,因为无法使已发布的令牌过期。出于同样的原因,您也无法实现空闲超时。一种解决方案是保留黑名单,但这会引入状态。
我更详细地写了post explaining these drawbacks。需要明确的是,您可以通过增加更多复杂性(滑动会话、刷新令牌等)来解决这些问题
至于其他选项,如果您的客户仅通过浏览器与您的服务交互,我强烈建议使用基于 cookie 的会话管理解决方案。我也compiled a list authentication methods目前在网上广泛使用。
【讨论】:
感谢链接/和创作它的优秀简单的身份验证指南:) 使用 JWT+Cookies 的组合(将 accessToken 保存到 cookie)会是一个好的解决方案吗? 将 JWT 保存到 cookie 效果很好。它将为您的 cookie 值完整性保护,但如果您需要支持更高级的场景(如空闲超时),您仍然需要一些方法来按需将令牌列入黑名单。我会在 cookie 中选择一个简单的会话 ID。【参考方案9】:好问题 - 问题本身包含丰富的信息。
文章Refresh Tokens: When to Use Them and How They Interact with JWTs 为这种情况提供了一个好主意。一些要点是:-
刷新令牌携带获得新访问权限所需的信息 令牌。 刷新令牌也可以过期,但寿命相当长。 刷新令牌通常受到严格的存储要求,以 确保它们没有泄漏。 它们也可以被授权服务器列入黑名单。也可以看看auth0/angular-jwtangularjs
用于 Web API。阅读Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin
【讨论】:
也许我读错了...但是标题以“刷新令牌...”开头的文章除了您在此处提到的内容外,没有任何关于刷新令牌的内容。【参考方案10】:我通过在令牌数据中添加一个变量解决了这个问题:
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 变量来设置一个最大日期以强制令牌过期,或者可以设置一个计数器,每当令牌更新时就会减少,从而限制令牌更新的总量。 【参考方案11】:这个方法怎么样:
对于每个客户端请求,服务器都会将令牌的过期时间与 (currentTime - lastAccessTime) 进行比较 如果 expirationTime ,它将 lastAccessedTime 更改为 currentTime。 如果浏览器不活动的持续时间超过 expireTime 或浏览器窗口关闭且 expirationTime > (currentTime - lastAccessedTime),则服务器可以使令牌过期并要求用户重新登录。在这种情况下,我们不需要额外的端点来刷新令牌。 非常感谢任何反馈。
【讨论】:
在这个时代是不是一个不错的选择,它看起来很容易实现。 在这种情况下,您将 lastAccessedTime 存储在哪里?您必须在后端和每个请求上执行此操作,因此它成为不理想的有状态解决方案。【参考方案12】:参考 - Refresh Expired JWT Example
另一种选择是,一旦 JWT 过期,用户/系统将调用 另一个 url 假设 /refreshtoken。与此请求一起,过期的 JWT 也应该被传递。然后,服务器将返回一个可供用户/系统使用的新 JWT。
【讨论】:
我认为像这样使用过期令牌进行身份验证没有多大意义。令牌可能会泄漏,因此更安全的做法是在过期之前使用仍然有效的 JWT 定期更新。【参考方案13】:我知道这是一个老问题,但我使用了会话和令牌身份验证的混合。我的应用程序是微服务的组合,因此我需要使用基于令牌的身份验证,这样每个微服务都不需要访问集中式数据库进行身份验证。我向我的用户发出 2 个 JWT(由不同的秘密签名):
-
标准 JWT,用于验证请求。此令牌将在 15 分钟后过期。
一个 JWT,用作放置在安全 cookie 中的刷新令牌。只有一个端点(实际上是它自己的微服务)接受这个令牌,它是 JWT 刷新端点。它必须伴随着帖子正文中的 CSRF 令牌,以防止该端点上的 CRSF。 JWT 刷新端点将会话存储在数据库中(会话的 id 和用户被编码到刷新 JWT 中)。这允许用户或管理员使刷新令牌无效,因为该令牌必须同时验证并匹配该用户的会话。
这工作得很好,但比仅使用基于会话的身份验证与 cookie 和 CSRF 令牌要复杂得多。因此,如果您没有微服务,那么基于会话的身份验证可能是可行的方法。
【讨论】:
【参考方案14】: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
;
);
【讨论】:
如果你能添加一些解释会很有帮助以上是关于JWT(JSON Web Token)自动延长过期时间的主要内容,如果未能解决你的问题,请参考以下文章