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

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

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

我发现Auth0是如何解决它的。他们不仅使用JWT令牌,还使用refresh令牌: # EYZ0 < / p >

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

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

270554 次浏览

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

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

Web应用程序

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

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

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

移动/本机应用程序

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

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

永不过期的令牌的问题是从来没有表示永不。如果你丢了手机怎么办?因此,它需要被用户以某种方式识别,应用程序需要提供一种撤销访问的方法。我们决定使用设备的名称,例如“maryo's ipad”。然后用户可以进入应用程序并撤销对“maryo's ipad”的访问权限。

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

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

在你自己处理认证的情况下(即不使用Auth0这样的提供者),以下方法可能有效:

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

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

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

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

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

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

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

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

这个方法怎么样:

  • 对于每个客户端请求,服务器将令牌的过期时间与(currentTime - lastAccessTime)进行比较。
  • 如果是expirationTime & lt;(currentTime - lastAccessedTime),它将lastAccessedTime更改为currentTime。
  • 如果浏览器上的不活动时间超过了expirationTime,或者如果浏览器窗口被关闭并且是the expirationTime > (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())

免责声明:我是维护者

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

文章刷新令牌:何时使用它们以及它们如何与jwt交互为这种情况提供了一个很好的想法。一些要点是:-

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

也可以看看auth0 / angular-jwt angularjs

用于Web API。读# EYZ0

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

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

如果短令牌过期了,但仍然是可信的,而长令牌是有效且可信的,它将使用长令牌验证的服务上的一个特殊端点刷新短令牌(这是它唯一可以用于的事情)。然后,它将使用短令牌来获得一个新的长令牌,从而在每次刷新短令牌时将其延长一周。

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

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

另一种使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)检查所提交的令牌是否仍然有效。如果它是无效的,服务器将响应此错误的状态,例如。# EYZ1。

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

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

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

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

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

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

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

1)当你登录时,发送2个token (Access token, Refresh token)来响应客户端 2) Access token的过期时间较短,Refresh的过期时间较长 3)客户端(前端)将刷新令牌存储在本地存储中,访问令牌存储在cookie中 4)客户端将使用访问令牌调用api。但是当它过期时,从本地存储中选择刷新令牌并调用认证服务器api来获得新的令牌 5)你的认证服务器将有一个暴露的api,它将接受刷新令牌并检查其有效性,并返回一个新的访问令牌 6)一旦刷新令牌过期,用户将被注销。< / p >

如果你需要更多细节,请告诉我,我也可以分享代码(Java + Spring引导)。

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

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

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

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

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

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

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

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

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
};
});
}

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

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

这工作得很好,但比使用基于会话的认证和cookie和CSRF令牌要复杂得多。因此,如果你没有微服务,那么基于会话的认证可能是可行的方法。

如果您正在使用AWS Amplify &Cognito这将为您带来神奇的效果:

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

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);
}

产地:# EYZ0