我希望对新的REST API实现基于jwt的身份验证。但是由于到期时间是在令牌中设置的,那么是否可以自动延长它呢?我不希望用户每隔X分钟就需要登录一次,如果他们在这段时间内积极使用应用程序的话。这将是一个巨大的用户体验失败。
但是延长过期时间会创建一个新的令牌(旧的令牌在过期前仍然有效)。在每个请求后生成一个新的令牌对我来说听起来很傻。当多个令牌同时有效时,听起来像是一个安全问题。当然,我可以使用黑名单使旧的使用无效,但我需要存储令牌。JWT的好处之一是没有存储空间。
我发现Auth0是如何解决这个问题的。他们不仅使用JWT令牌,还使用refresh令牌:
https://auth0.com/docs/tokens/refresh-tokens
但是,要实现这一点(没有Auth0),我需要存储刷新令牌并维护它们的过期。那么真正的好处是什么呢?为什么不只有一个令牌(不是JWT)并将过期时间保存在服务器上呢?
还有其他选择吗?使用JWT不适合这种情况吗?
实际上,我使用Guzzle客户端在PHP中实现了这一点,为api制作了一个客户端库,但这个概念应该适用于其他平台。
基本上,我发行了两个代币,一个短的(5分钟),一个长的,一周后到期。如果客户机库接收到对某个请求的401响应,则使用中间件尝试对短令牌进行一次刷新。然后,它将再次尝试原始请求,如果它能够刷新,就会获得正确的响应,对用户透明。如果失败了,它会把401发给用户。
如果短令牌过期了,但仍然是可信的,而长令牌是有效且可信的,它将使用长令牌验证的服务上的一个特殊端点刷新短令牌(这是它唯一可以用于的事情)。然后,它将使用短令牌来获得一个新的长令牌,从而在每次刷新短令牌时将其延长一周。
这种方法还允许我们在最多5分钟内撤销访问,这对于我们的使用是可以接受的,而不必存储令牌黑名单。
后期编辑:在我脑海中记忆犹新的几个月后重新阅读这篇文章,我应该指出,你可以在刷新短令牌时撤销访问权,因为它为更昂贵的调用提供了机会(例如调用数据库来查看用户是否已被禁止),而无需为每一次对你的服务的调用付费。
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的点击。
我知道这是一个老问题,但我同时使用会话和令牌身份验证。我的应用程序是微服务的组合,所以我需要使用基于令牌的身份验证,这样每个微服务都不需要访问集中的数据库进行身份验证。我向我的用户发出2个jwt(由不同的秘密签名):
A standard JWT, used to authenticate requests. This token expires after 15 minutes.
A JWT that acts as a refresh token that is placed in a secure cookie. Only one endpoint (actually it is its own microservice) accepts this token, and it is the JWT refresh endpoint. It must be accompanied by a CSRF token in the post body to prevent CRSF on that endpoint. The JWT refresh endpoint stores a session in the database (the id of the session and the user are encoded into the refresh JWT). This allows the user, or an admin, to invalidate a refresh token as the token must both validate and match the session for that user.
这工作得很好,但比使用基于会话的认证和cookie和CSRF令牌要复杂得多。因此,如果你没有微服务,那么基于会话的认证可能是可行的方法。