JWT的刷新问题
想对新REST API实施基于JWT的身份验证,但是令牌中设置了到期时间,感觉使用不太方便,那么问题来了,是否可以自动延长这个过期时间呢?如果用户频繁使用应用程序,就不希望用户每X分钟后需要重新登录。老是要用户登录,从用户体验的角度来说,肯定是非常糟糕的。
但延长过期时间会创建一个新的令牌(而且同时旧的令牌仍然有效,直到它过期)。并且在每个请求之后生成一个新的令牌,感觉是比较愚蠢的做法。另外,当有多个令牌同时有效时,感觉还会有安全问题。当然,我可以使用黑名单使旧的令牌失效,但是我需要存储令牌。而JWT的优点之一就是无需存储空间。
我找到了Auth0如何解决它的方式。他们不仅使用JWT令牌,还会刷新令牌:https://docs.auth0.com/refresh-token
但是还是同样的问题,为了实现这一点(不使用Auth0),我需要存储刷新令牌并维护它们的到期时间。那么这么做到底有什么用呢?为什么不只有一个令牌(不是JWT)并且在服务器上保持到期?
是否还有其他选择?是不是JWT不适合这种情况呢?
最佳解决方法
我在Auth0公司工作,我参与了刷新令牌功能的设计。
这一切都取决于应用的类型,下面是我们推荐的方法。
Web应用程序
一个好的模式是在它过期之前刷新令牌。
将令牌过期时间设置为一周,并在每次用户打开Web应用程序并每隔一小时刷新令牌。如果用户超过一周没有打开过应用程序,那他们就需要再次登录,这是可接受的Web应用程序UX(用户体验)。
要刷新令牌,API需要一个新的端点,它接收一个有效的、没有过期的JWT、并返回与新的到期字段相同的签名的JWT。然后Web应用程序会将令牌存储在某处。
移动/本地应用程序
大多数本地应用程序的登录有且仅有一次。
这里面的出发点是,刷新令牌永远不会过期,并且可以始终为有效的JWT进行更换。
永远不会过期的令牌的问题是它失去了令牌的意义。譬如,如果你电话丢了,你该怎么办?因此,它需要由用户以某种方式进行识别,应用程序需要提供撤销访问的方法。我们决定使用设备的名称,例如“maryo的iPad”。然后用户可以去应用程序,并撤销访问“maryo的iPad”。
另一种方法是撤销特定事件的刷新令牌,其中一个有趣的事件是更改密码。
我们认为JWT对于这些用例无效,因此我们使用随机生成的字符串,并将它们存储在我们这边。
次佳解决思路
在您自己处理身份验证(即不要使用Auth0等提供者)的情况下,可能需要执行以下操作:
-
发送JWT令牌的时间相对较短,比如15分钟。
-
应用程序在需要令牌的任何事务(令牌包含到期日期)之前检查令牌到期日。如果令牌已过期,那么它首先要求API刷新令牌。从用户体验的角度来说,这是完全透明的。
-
API获取令牌刷新请求,但首先检查用户数据库以查看是否针对该用户配置文件设置了’reauth’标志(令牌可以包含用户标识)。如果标志存在,则令牌刷新被拒绝,否则发出新的令牌。
-
重复上述过程。
数据库后端的’reauth’标志在例如用户已经重置密码时被设置。当用户下次登录时,该标志将被删除。
此外,假设您有一个策略,用户必须至少每72小时登录一次。在这种情况下,您的API令牌刷新逻辑还将从用户数据库中检查用户的最后登录日期,并在此基础上拒绝/允许令牌刷新。
第三种解决思路
在后端使用RESTful APIs将应用程序迁移到HTML5时,我正在做相关的修修补补的工作。我想出的解决办法是:
-
在成功登录后,客户端会发出会话时间为30分钟(或任何常用的服务器端会话时间)的令牌。
-
创建一个客户端计时器来调用服务在其到期之前更新令牌。新的令牌将取代以后的调用。
如您所见,这样可以减少频繁更新令牌请求。如果用户在更新令牌调用触发之前关闭浏览器/应用程序,则先前的令牌将及时到期,用户将必须重新登录。
可以实现更复杂的策略来满足用户不活动时的情形(例如忽略打开的浏览器选项卡)。在这种情况下,续订令牌调用应包括预期到期时间,而这个时间不应超过定义的会话时间。那么应用程序必须相应地跟踪上一次用户的交互。
我不喜欢设置长期到期时间的思路,因为这种方法对于不需要那么频繁身份验证的本机应用程序可能无法正常工作。
其他解决思路
jwt-autorefresh(JWT自动刷新)
如果您正在使用这种节点节点(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())
免责声明:我是维护者
附: