对于我正在从事的一个新的node.js项目,我正在考虑从基于cookie的会话方法(我的意思是,将id存储到用户浏览器中包含用户会话的键值存储中)切换到使用JSON Web Tokens (jwt)的基于令牌的会话方法(没有键值存储)。
这个项目是一个利用socket的游戏。IO——在一个会话(web和socket.io)中有多个通信通道的情况下,有一个基于令牌的会话会很有用。
如何使用jwt方法从服务器提供令牌/会话失效?
我还想了解使用这种范例应该注意哪些常见的(或不常见的)陷阱/攻击。例如,如果这种模式容易受到与基于会话存储/cookie的方法相同/不同类型的攻击。
所以,假设我有以下内容(改编自this和this):
会话存储登录:
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
// Create session token
var token= createSessionToken();
// Add to a key-value database
KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});
// The client should save this session token in a cookie
response.json({sessionToken: token});
});
}
口令登录:
var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
response.json({token: token});
});
}
--
会话存储方法的注销(或失效)需要更新KeyValueStore
使用指定的令牌创建数据库。
在基于令牌的方法中似乎不存在这样的机制,因为令牌本身将包含通常存在于键值存储中的信息。
如果您希望能够撤销用户令牌,您可以跟踪DB上所有发出的令牌,并检查它们在类似会话的表上是否有效(存在)。
缺点是每次请求都要访问DB。
我还没有尝试过,但我建议使用以下方法来允许令牌撤销,同时将DB命中保持在最小值-
为了降低数据库检查率,根据某种确定性关联将所有已发行的JWT令牌分成X组(例如,按用户id的第一个数字分为10组)。
每个JWT令牌将保存组id和令牌创建时创建的时间戳。例如,{"group_id": 1, "timestamp": 1551861473716}
服务器将在内存中保存所有组id,每个组都有一个时间戳,该时间戳指示属于该组的用户的最后一次注销事件是什么时候。
例如,{"group1": 1551861473714, "group2": 1551861487293,…}
使用带有较旧组时间戳的JWT令牌的请求将被检查是否有效(DB hit),如果有效,将发出一个带有新时间戳的新JWT令牌供客户端将来使用。
如果令牌的组时间戳较新,则相信JWT (No DB hit)。
So -
We only validate a JWT token using the DB if the token has an old group timestamp, while future requests won't get validated until someone in the user's group will log-out.
We use groups to limit the number of timestamp changes (say there's a user logging in and out like there's no tomorrow - will only affect limited number of users instead of everyone)
We limit the number of groups to limit the amount of timestamps held in memory
Invalidating a token is a breeze - just remove it from the session table and generate a new timestamp for the user's group.
我要回答的是,当我们使用JWT时,我们是否需要提供从所有设备注销功能。这种方法将对每个请求使用数据库查找。因为即使服务器崩溃,我们也需要持久安全状态。在用户表中,我们将有两列
LastValidTime(默认为创建时间)
登录(默认值:true)
每当有来自用户的注销请求时,我们将LastValidTime更新为当前时间,login - in更新为false。如果有一个登录请求,我们不会改变LastValidTime,但登录将被设置为true。
当我们创建JWT时,我们将在有效载荷中有JWT创建时间。当我们授权一项服务时,我们将检查3个条件
JWT有效吗
JWT有效负载创建时间是否大于用户LastValidTime
用户是否已登录
让我们来看一个实际的场景。
用户X有两个设备A和b,他在晚上7点使用设备A和设备b登录到我们的服务器(假设JWT过期时间是12小时)。A和B都有JWT, createdTime: 7pm
在晚上9点,他丢失了他的设备b,他立即从设备a注销。这意味着现在我们的数据库X用户条目的LastValidTime为“ThatDate:9:00:xx:xxx”,登录为“false”。
在9:30,Mr.Thief尝试使用设备b登录,我们将检查数据库,即使登录是假的,所以我们不允许。
晚上10点,x先生从他的设备A登录,现在设备A有JWT,并创建了时间:10点。现在database Logged-In被设置为true
晚上10点半,小偷先生试图登录。即使“登录”是真的。数据库中的LastValidTime是晚上9点,但是B的JWT创建的时间是晚上7点。所以他不会被允许访问该服务。所以使用设备B没有密码,他不能使用已经创建的JWT在一个设备注销后。
没有使用jwt的刷新…
我想到了两种袭击的场景。一个是关于登录凭证的泄露。另一个是对JWT的盗窃。
For compromised login credentials, when a new login happens, normally send the user an email notification. So, if the customer doesn't consent to being the one who logged in, they should be advised to do a reset of credentials, which should save to database/cache the date-time the password was last set (and set this too when user sets password during initial registration). Whenever a user action is being authorized, the date-time a user changed their password should be fetched from database/cache and compared to the date-time a given JWT was generated, and forbid the action for JWTs that were generated before the said date-time of credentials reset, hence essentially rendering such JWTs useless. That means save the date-time of generation of a JWT as a claim in the JWT itself. In ASP.NET Core, a policy/requirement can be used to do do this comparison, and on failure, the client is forbidden. This consequently logs out the user on the backend, globally, whenever a reset of credentials is done.
For actual theft of JWT... A theft of JWT is not easy to detect but a JWT that expires easily solves this. But what can be done to stop the attacker before the JWT expires? It is with an actual global logout. It is similar to what was described above for credentials reset. For this, normally save on database/cache the date-time a user initiated a global logout, and on authorizing a user action, get it and compare it to the date-time of generation of a given JWT too, and forbid the action for JWTs that were generated before the said date-time of global logout, hence essentially rendering such JWTs useless. This can be done using a policy/requirement in ASP.NET Core, as previously described.
现在,你如何发现JWT被盗?目前我对此的回答是,偶尔提醒用户全局注销并重新登录,因为这肯定会让攻击者注销。
使用刷新jwt…
我采用的一种比较实用的方法是在数据库中存储一个刷新令牌(可以是GUID)和对应的刷新令牌ID(无论进行多少次刷新都不会改变),并在生成用户的JWT时将它们作为用户的声明添加。可以使用数据库的替代方案,例如内存缓存。但我用的是数据库。
然后,创建一个JWT刷新Web API端点,客户机可以在JWT到期之前调用该端点。当调用刷新时,从JWT中的声明中获取刷新令牌。
在对JWT刷新端点的任何调用中,在数据库上验证当前刷新令牌和刷新令牌ID为一对。生成一个新的刷新令牌,并使用刷新令牌ID替换数据库上的旧刷新令牌。记住它们是可以从JWT中提取出来的声明
从当前JWT中提取用户的声明。开始生成一个新的JWT的过程。将旧的刷新令牌声明的值替换为新生成的刷新令牌,该刷新令牌也是新保存在数据库上的。完成所有这些后,生成新的JWT并将其发送给客户端。
因此,在使用了刷新令牌之后,无论是目标用户还是攻击者,在数据库上使用未与其刷新令牌ID配对的/刷新令牌的任何其他尝试都不会导致生成新的JWT,从而阻止任何拥有该刷新令牌ID的客户端不再能够使用后端,从而导致此类客户端(包括合法客户端)的完全注销。
这解释了基本信息。
The next thing to add to that is to have a window for when a JWT can be refreshed, such that anything outside that window would be a suspicious activity. For example, the window can be 10min before the expiration of a JWT. The date-time a JWT was generated can be saved as a claim in that JWT itself. And when such suspicious activity occurs, i.e. when someone else tries to reuse that refresh token ID outside or within the window after it has already been used within the window, should mark the refresh token ID as invalid. Hence, even the valid owner of the refresh token ID would have to log in afresh.
如果在数据库上找不到与所提供的刷新令牌ID配对的刷新令牌,则表明刷新令牌ID应该无效。因为空闲用户可能会尝试使用攻击者已经使用过的刷新令牌。
如前所述,在目标用户之前被攻击者窃取和使用的JWT,在用户尝试使用刷新令牌时也会被标记为无效。
唯一没有涉及的情况是,即使攻击者可能已经窃取了JWT,客户端也从未尝试刷新它。但是这种情况不太可能发生在不受攻击者监管(或类似)的客户端上,这意味着攻击者无法预测客户端何时停止使用后端。
如果客户端发起常规注销。应该通过注销从数据库中删除刷新令牌ID和相关记录,从而防止任何客户端生成刷新JWT。
这主要是一个很长的评论,支持并建立在@mattway的回答上
考虑到:
本页上提出的其他一些解决方案提倡对每个请求都访问数据存储。如果您使用主数据存储来验证每个身份验证请求,那么我认为使用JWT而不是其他已建立的令牌身份验证机制的理由就更少了。如果每次都访问数据存储,那么实际上JWT是有状态的,而不是无状态的。
(如果您的站点接收到大量未经授权的请求,那么JWT将在不访问数据存储的情况下拒绝它们,这很有帮助。可能还有其他类似的用例。)
考虑到:
真正的无状态JWT身份验证无法在典型的、真实的web应用程序中实现,因为无状态JWT无法为以下重要用例提供即时和安全的支持:
用户帐号被删除/屏蔽/挂起。
完成用户密码的修改。
用户角色或权限发生变更。
用户被admin注销。
JWT令牌中的任何其他应用程序关键数据都由站点管理员更改。
在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否是出于恶意。
因此:
我认为来自@mat -way的答案,#2 TokenBlackList,将是向基于JWT的身份验证添加所需状态的最有效方法。
您有一个黑名单保存这些令牌,直到它们的过期日期。与用户总数相比,代币列表将非常小,因为它只需要保留黑名单上的代币直到到期。我将通过在redis、memcached或其他支持设置密钥过期时间的内存数据存储中放置无效的令牌来实现。
对于每个通过初始JWT身份验证的身份验证请求,仍然需要调用内存中的数据库,但不必将整个用户集的密钥存储在其中。(对于一个特定的网站来说,这可能是也可能不是什么大问题。)