对于我正在从事的一个新的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.
Kafka消息队列和本地黑名单
我想过使用像kafka这样的消息系统。让我解释一下:
例如,您可以有一个微服务(我们称之为userMgmtMs服务),它负责登录和注销,并生成JWT令牌。然后将这个令牌传递给客户端。
现在客户端可以使用这个令牌来调用不同的微服务(让我们称之为pricesMs),在pricesMs中,将没有数据库检查用户表,从这个表中触发了最初的令牌创建。该数据库只能存在于userMgmtMs中。此外,JWT令牌应该包括权限/角色,这样pricesm就不需要从DB中查找任何东西来允许spring安全性工作。
JwtRequestFilter可以提供一个UserDetails对象,该对象由JWT令牌中提供的数据创建(显然没有密码),而不是去到pricesMs中的DB。
那么,如何注销或使令牌失效呢?由于我们不想在每次请求priecesm时都调用userMgmtMs的数据库(这会引入很多不必要的依赖关系),因此解决方案可以使用这个令牌黑名单。
我建议使用kafka消息队列,而不是保持这个黑名单集中,并依赖于所有微服务的一个表。
userMgmtMs仍然负责注销,一旦注销完成,它就会把它放入自己的黑名单(一个微服务之间不共享的表)。此外,它还将带有此令牌内容的kafka事件发送到订阅了所有其他微服务的内部kafka服务。
一旦其他微服务接收到kafka事件,它们也会将其放入内部黑名单。
即使一些微服务在注销时关闭了,它们最终也会重新启动,并在稍后的状态下接收消息。
由于kafka是这样开发的,客户端有他们自己的引用,他们读了哪些消息,确保没有客户端,down或up将错过任何这些无效的令牌。
The only issue again what I can think of is that the kafka messaging service will again introduce a single point of failure. But it is kind of reversed because if we have one global table where all invalid JWT tokens are saved and this db or micro service is down nothing works. With the kafka approach + client side deletion of JWT tokens for a normal user logout a downtime of kafka would in most cases not even be noticeable. Since the black lists are distributed among all microservies as an internal copy.
在关闭的情况下,你需要使一个被黑客攻击的用户无效,kafka宕机了,这就是问题开始的地方。在这种情况下,作为最后的手段改变秘密可能会有所帮助。或者在这样做之前确保卡夫卡已经起床了。
免责声明:我还没有实现这个解决方案,但不知怎么的,我觉得大多数提议的解决方案都否定了JWT令牌的想法,因为它有一个中央数据库查找。所以我在考虑另一种解决方案。
请让我知道你的想法,它是有意义的还是有一个明显的原因为什么它不能?
Haven't tried this yet, and it is uses a lot of information based on some of the other answers. The complexity here is to avoid a server side data store call per request for user information. Most of the other solutions require a db lookup per request to a user session store. That is fine in certain scenarios but this was created in an attempt to avoid such calls and make whatever required server side state to be very small. You will end up recreating a server side session, however small to provide all the force invalidation features. But if you want to do it here is the gist:
目标:
减少数据存储的使用(无状态)。
能够强制注销所有用户。
能力强制注销任何个人在任何时间。
在一段时间后要求密码重新输入的能力。
能够与多个客户一起工作。
当用户从特定客户端单击注销时,强制重新登录的能力。(为了防止有人在用户离开后“取消删除”客户端令牌-查看评论了解更多信息)
解决方案:
Use short lived (<5m) access tokens paired with a longer lived (few hours) client stored refresh-token.
Every request checks either the auth or refresh token expiration date for validity.
When the access token expires, the client uses the refresh token to refresh the access token.
During the refresh token check, the server checks a small blacklist of user ids - if found reject the refresh request.
When a client doesn't have a valid(not expired) refresh or auth token the user must log back in, as all other requests will be rejected.
On login request, check user data store for ban.
On logout - add that user to the session blacklist so they have to log back in. You would have to store additional information to not log them out of all devices in a multi device environment but it could be done by adding a device field to the user blacklist.
To force re-entry after x amount of time - maintain last login date in the auth token, and check it per request.
To force log out all users - reset token hash key.
这要求您在服务器上维护一个黑名单(状态),假设用户表包含禁止的用户信息。无效会话黑名单-是一个用户id列表。此黑名单仅在刷新令牌请求期间检查。只要刷新令牌TTL存在,条目就必须存在于该节点上。刷新令牌过期后,用户将被要求重新登录。
缺点:
仍然需要对刷新令牌请求执行数据存储查找。
无效的令牌可能会继续为访问令牌的TTL操作。
优点:
提供所需的功能。
在正常操作下,刷新令牌动作对用户隐藏。
只需要对刷新请求而不是每个请求执行数据存储查找。即每15分钟1次,而不是每秒1次。
最小化服务器端状态到一个非常小的黑名单。
With this solution an in memory data store like reddis isn't needed, at least not for user information as you are as the server is only making a db call every 15 or so minutes. If using reddis, storing a valid/invalid session list in there would be a very fast and simpler solution. No need for a refresh token. Each auth token would have a session id and device id, they could be stored in a reddis table on creation and invalidated when appropriate. Then they would be checked on every request and rejected when invalid.
如果您希望能够撤销用户令牌,您可以跟踪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.