我希望对新的REST API实现基于jwt的身份验证。但是由于到期时间是在令牌中设置的,那么是否可以自动延长它呢?我不希望用户每隔X分钟就需要登录一次,如果他们在这段时间内积极使用应用程序的话。这将是一个巨大的用户体验失败。
但是延长过期时间会创建一个新的令牌(旧的令牌在过期前仍然有效)。在每个请求后生成一个新的令牌对我来说听起来很傻。当多个令牌同时有效时,听起来像是一个安全问题。当然,我可以使用黑名单使旧的使用无效,但我需要存储令牌。JWT的好处之一是没有存储空间。
我发现Auth0是如何解决这个问题的。他们不仅使用JWT令牌,还使用refresh令牌:
https://auth0.com/docs/tokens/refresh-tokens
但是,要实现这一点(没有Auth0),我需要存储刷新令牌并维护它们的过期。那么真正的好处是什么呢?为什么不只有一个令牌(不是JWT)并将过期时间保存在服务器上呢?
还有其他选择吗?使用JWT不适合这种情况吗?
今天,许多人选择使用jwt进行会话管理,却没有意识到他们为了简单而放弃了什么。我的回答详细阐述了问题的第二部分:
那么真正的好处是什么呢?为什么不只有一个令牌(不是JWT)并将过期时间保存在服务器上呢?
还有其他选择吗?使用JWT不适合这种情况吗?
jwt能够支持基本的会话管理,但有一些限制。由于是自描述令牌,它们在服务器端不需要任何状态。这使得他们很有吸引力。例如,如果服务没有持久层,它就不需要仅仅为了会话管理而引入持久层。
然而,无国籍也是他们缺点的主要原因。由于它们只发布一次,内容固定且到期,因此您无法使用典型的会话管理设置完成您想做的事情。
也就是说,您不能按需使它们失效。这意味着您无法实现安全注销,因为没有办法使已经发出的令牌过期。出于同样的原因,您也不能实现空闲超时。一个解决方案是保留一个黑名单,但这会引入状态。
我写了一篇文章详细解释了这些缺点。需要明确的是,您可以通过添加更复杂的内容(滑动会话、刷新令牌等)来解决这些问题。
至于其他选项,如果您的客户端仅通过浏览器与您的服务交互,我强烈建议使用基于cookie的会话管理解决方案。我还列出了目前在web上广泛使用的认证方法。
我在Auth0工作,我参与了刷新令牌功能的设计。
这完全取决于应用程序的类型,这里是我们推荐的方法。
Web应用程序
一个好的模式是在令牌过期之前刷新令牌。
将令牌过期时间设置为一周,并在用户每次打开web应用程序和每一小时刷新令牌。如果用户超过一周没有打开应用程序,他们将不得不再次登录,这是可接受的web应用程序UX。
要刷新令牌,您的API需要一个新的端点,该端点接收有效的、未过期的JWT,并返回带有新过期字段的签名JWT。然后web应用程序将把令牌存储在某个地方。
移动/本机应用程序
大多数本机应用程序只登录一次。
其思想是,刷新令牌永远不会过期,并且可以始终将其交换为有效的JWT。
永不过期的令牌的问题在于,它永远不会意味着永远不会。如果你丢了手机怎么办?因此,它需要被用户以某种方式识别,应用程序需要提供一种撤销访问的方法。我们决定使用设备的名称,例如。玛丽:iPad。然后用户可以进入应用程序,撤销对“maryo的iPad”的访问权限。
另一种方法是撤销特定事件上的刷新令牌。一个有趣的事件是修改密码。
我们认为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())
免责声明:我是维护者
JWT的想法很好,你把你需要的东西放进JWT,然后无状态化。
两个问题:
糟糕的JWT标准化。
JWT是不可能失效的,如果创建快速到期,它会迫使用户频繁登录。
1的解。使用自定义JSON:
{"userId": "12345", "role": "regular_user"}
使用对称(AES)算法加密它(它比使用非对称算法签名更快),并将其放入快速过期的cookie中。我仍然会称它为JWT,因为它是JSON,并且在Web应用程序中用作令牌。现在,服务器检查cookie是否存在,其值是否可以解密。
2的解。使用刷新令牌:
将userId作为12345,对其加密,并将其放入长期过期的cookie中。不需要在DB中为刷新令牌创建一个特殊字段。
现在,每次访问令牌(JWT) cookie过期时,服务器都会检查刷新令牌cookie,解密,获取值,并在DB中查找用户。如果找到用户,则生成一个新的访问令牌,否则(或者如果刷新令牌也过期)强制用户登录。
最简单的替代方法是使用刷新令牌作为访问令牌,即根本不使用JWT。
使用JWT的优点是,在它的过期时间内,服务器不会访问DB。即使我们在cookie中放入一个过期时间只有2分钟的访问令牌,对于像eBay这样繁忙的应用程序,它也会导致每秒避免数千DB的点击。