使JSON Web令牌失效

对于我正在从事的一个新的node.js项目,我正在考虑从基于cookie的会话方法(我的意思是,将id存储到用户浏览器中包含用户会话的键值存储中)切换到使用JSON Web Tokens (jwt)的基于令牌的会话方法(没有键值存储)。

这个项目是一个利用socket的游戏。IO——在一个会话(web和socket.io)中有多个通信通道的情况下,有一个基于令牌的会话会很有用。

如何使用jwt方法从服务器提供令牌/会话失效?

我还想了解使用这种范例应该注意哪些常见的(或不常见的)陷阱/攻击。例如,如果这种模式容易受到与基于会话存储/cookie的方法相同/不同类型的攻击。

所以,说我有以下(改编自):

会话存储登录:

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

在基于令牌的方法中似乎不存在这样的机制,因为令牌本身将包含通常存在于键值存储中的信息。

377531 次浏览

我也一直在研究这个问题,虽然下面的想法都不是完整的解决方案,但它们可能会帮助其他人排除这些想法,或者提供进一步的解决方案。

1)简单地从客户端删除令牌

显然,这对服务器端安全没有任何帮助,但它确实通过删除令牌来阻止攻击者。他们必须在登出之前窃取令牌)。

2)创建一个令牌区块列表

您可以将无效令牌存储到它们最初的到期日期,并将它们与传入的请求进行比较。这似乎否定了首先完全基于令牌的原因,因为每个请求都需要访问数据库。存储大小可能会更小,因为您只需要存储注销&过期时间(这是一种直觉,当然取决于上下文)。

3)保持代币到期时间短,并经常轮换

如果您将令牌过期时间保持在足够短的间隔内,并让运行中的客户端保持跟踪并在必要时请求更新,那么第1条将有效地作为一个完整的注销系统。这种方法的问题是,它不可能在客户端代码关闭之间保持用户登录(取决于您设置的过期间隔多长时间)。

应急计划

如果出现紧急情况,或者用户令牌被破坏,您可以做的一件事是允许用户使用其登录凭据更改底层用户查找ID。这将使所有关联的令牌无效,因为将不再能够找到关联的用户。

我还想指出,在令牌中包含最后一次登录日期是个好主意,这样您就可以在一段时间后强制重新登录。

关于使用令牌的攻击的相似/不同之处,这篇文章解决了这个问题:https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies-vs-token.markdown

我会在用户模型上保存jwt版本号的记录。新的jwt令牌将其版本设置为此。

在验证jwt时,只需检查它的版本号是否等于用户当前的jwt版本。

任何时候你想要让旧的jwt失效,只要改变用户的jwt版本号。

上面的想法很好,但是有一种非常简单的方法可以使所有现有的jwt无效,那就是简单地改变秘密。

如果您的服务器创建了JWT,用一个秘密(JWS)对其进行签名,然后将其发送给客户端,简单地更改秘密将使所有现有的令牌无效,并要求所有用户获得一个新的令牌进行身份验证,因为他们的旧令牌突然根据服务器变得无效。

它不需要对实际的令牌内容(或查找ID)进行任何修改。

显然,这仅适用于当您希望所有现有令牌过期时的紧急情况,对于每个令牌过期,需要上述解决方案之一(例如短令牌过期时间或令牌内存储的密钥无效)。

我一直在考虑的一种方法是在JWT中始终有一个iat(发布时间)值。然后,当用户注销时,将时间戳存储在用户记录上。在验证JWT时,只需将iat与上次注销的时间戳进行比较。如果iat较旧,则它无效。是的,您必须访问DB,但如果JWT在其他方面有效,我将始终提取用户记录。

我所看到的主要缺点是,如果他们在多个浏览器中,或者有一个移动客户端,它会让他们退出所有的会话。

这也是一种很好的机制,可以使系统中的所有jwt无效。部分检查可以针对最后有效的iat时间的全局时间戳。

我来晚了一点,但我想我有个不错的解决办法。

我在数据库中有一个“last_password_change”列,用于存储密码最后一次更改的日期和时间。我还将发布日期/时间存储在JWT中。在验证令牌时,我会检查在令牌发出后密码是否已更改,如果已更改,即使令牌尚未过期也会被拒绝。

为什么不直接使用jti声明(nonce)并将其作为用户记录字段存储在列表中(依赖于db,但至少是逗号分隔的列表)?不需要单独查找,正如其他人指出的那样,假设您无论如何都想获得用户记录,这样您就可以为不同的客户端实例拥有多个有效的令牌(“到处注销”可以将列表重置为空)

我还没试过这个,它使用了很多基于其他答案的信息。这里的复杂性在于避免每次请求用户信息时调用服务器端数据存储。大多数其他解决方案都需要对用户会话存储的每个请求进行db查找。这在某些情况下是可以的,但创建这个是为了避免这样的调用,并使所需的服务器端状态非常小。# EYZ0

目标:

  • 减少数据存储的使用(无状态)。
  • 能够强制注销所有用户。
  • 能力强制注销任何个人在任何时间。
  • 在一段时间后要求密码重新输入的能力。
  • 能够与多个客户一起工作。
  • 当用户从特定客户端单击注销时,强制重新登录的能力。(为了防止有人在用户离开后“取消删除”客户端令牌-查看评论了解更多信息)

# EYZ0

  • 使用短寿命(5m)访问令牌与较长寿命(几个小时)的客户端存储refresh-token配对。
  • 每个请求都检查认证或刷新令牌的有效期。
  • 当访问令牌过期时,客户端使用刷新令牌刷新访问令牌。
  • 在刷新令牌检查期间,服务器检查一个小的用户id黑名单-如果发现拒绝刷新请求。
  • 当客户端没有有效的(未过期的)刷新或认证令牌时,用户必须重新登录,因为所有其他请求都将被拒绝。
  • 在登录请求时,检查用户数据存储是否禁止。
  • 注销时-将该用户添加到会话黑名单,以便他们必须重新登录。在多设备环境中,您必须存储额外的信息,以免将他们从所有设备上注销,但这可以通过向用户黑名单中添加设备字段来实现。
  • 要在x时间后强制重新进入,请在认证令牌中维护上次登录日期,并在每个请求中检查它。
  • 要强制注销所有用户-重置令牌哈希键。

这要求您在服务器上维护一个黑名单(状态),假设用户表包含禁止的用户信息。无效会话黑名单-是一个用户id列表。此黑名单仅在刷新令牌请求期间检查。只要刷新令牌TTL存在,条目就必须存在于该节点上。刷新令牌过期后,用户将被要求重新登录。

缺点:

  • 仍然需要对刷新令牌请求执行数据存储查找。
  • 无效的令牌可能会继续为访问令牌的TTL操作。

优点:

  • 提供所需的功能。
  • 在正常操作下,刷新令牌动作对用户隐藏。
  • 只需要对刷新请求而不是每个请求执行数据存储查找。即每15分钟1次,而不是每秒1次。
  • 最小化服务器端状态到一个非常小的黑名单。

在这个解决方案中,不需要像reddis这样的内存数据存储,至少不需要存储用户信息,因为服务器每15分钟左右才会调用一次db。如果使用reddis,存储有效/无效会话列表将是一个非常快速和简单的解决方案。不需要刷新令牌。每个认证令牌都有一个会话id和设备id,它们可以在创建时存储在redis表中,并在适当的时候失效。然后他们将对每个请求进行检查,并在无效时被拒绝。

这主要是一个支持和构建回复@mattway的长评论

考虑到:

本页上提出的其他一些解决方案提倡对每个请求都访问数据存储。如果您使用主数据存储来验证每个身份验证请求,那么我认为使用JWT而不是其他已建立的令牌身份验证机制的理由就更少了。如果每次都访问数据存储,那么实际上JWT是有状态的,而不是无状态的。

(如果您的站点接收到大量未经授权的请求,那么JWT将在不访问数据存储的情况下拒绝它们,这很有帮助。可能还有其他类似的用例。)

考虑到:

真正的无状态JWT身份验证无法在典型的、真实的web应用程序中实现,因为无状态JWT无法为以下重要用例提供立即安全支持:

  • 用户账号被删除/阻止/挂起。

  • 用户密码被修改。

  • 用户角色或权限发生改变。

  • 用户被admin注销。

  • JWT令牌中的任何其他应用程序关键数据都由站点管理员更改。

在这些情况下,您不能等待令牌到期。令牌失效必须立即发生。此外,您不能相信客户端不会保留和使用旧令牌的副本,无论是否是出于恶意。

因此:

我认为来自@mat -way的答案,#2 TokenBlackList,将是向基于JWT的身份验证添加所需状态的最有效方法。

您有一个黑名单保存这些令牌,直到它们的过期日期。与用户总数相比,代币列表将非常小,因为它只需要保留黑名单上的代币直到到期。我将通过在redis、memcached或其他支持设置密钥过期时间的内存数据存储中放置无效的令牌来实现。

对于每个通过初始JWT身份验证的身份验证请求,仍然需要调用内存中的数据库,但不必将整个用户集的密钥存储在其中。(对于一个特定的网站来说,这可能是也可能不是什么大问题。)

您可以在用户的文档/记录上的DB上有一个“last_key_used”字段。

当用户使用user和pass登录时,生成一个新的随机字符串,将其存储在last_key_used字段中,并在签名令牌时将其添加到负载中。

当用户使用令牌登录时,检查数据库中的last_key_used是否与令牌中的last_key_used匹配。

然后,当用户执行注销操作时,或者如果您想使令牌失效,只需将“last_key_used”字段更改为另一个随机值,随后的任何检查都将失败,从而迫使用户使用user登录并再次通过。

每个用户字符串唯一,全局字符串散列在一起

作为JWT的秘密部分,允许个人和全局令牌失效。最大的灵活性,代价是在请求身份验证期间数据库查找/读取。也很容易缓存,因为它们很少改变。

这里有一个例子:

HEADER:ALGORITHM & TOKEN TYPE


{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD:DATA


{
"sub": "1234567890",
"some": "data",
"iat": 1516239022
}
VERIFY SIGNATURE


HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
HMACSHA256('perUserString'+'globalString')
)


where HMACSHA256 is your local crypto sha256
nodejs
import sha256 from 'crypto-js/sha256';
sha256(message);

例如使用https://jwt.io(不确定他们处理动态256位秘密)

我是这样做的:

  1. 生成一个unique hash,然后将它存储在复述,JWT中。这可以称为会话
    • 我们还将存储特定的JWT所生成的请求的数量——每次jwt被发送到服务器时,我们都会增加请求整数。(这是可选的)
    • 李< / ul > < / >

因此,当用户登录时,将创建一个唯一的散列,存储在redis中并注入到JWT中。

当用户试图访问受保护的端点时,您将从JWT中获取唯一的会话散列,查询redis并查看它是否匹配!

我们可以在此基础上进行扩展,使我们的JWT更加安全,如下所示:

每个X请求一个特定的JWT,我们生成一个新的唯一会话,存储在我们的JWT,然后黑名单前一个。

这意味着JWT是不断变化的,并阻止陈旧的JWT被黑客攻击、窃取或其他东西。

  1. 代币的有效期为1天
  2. 每天建立一个黑名单。
  3. 将无效/注销令牌放入黑名单

对于令牌验证,首先检查令牌的过期时间,如果令牌没有过期,则检查黑名单。

对于长会话需求,应该有一种机制来延长令牌过期时间。

迟来的派对,经过一些研究,我的两点意见如下。 在注销期间,确保发生以下事情

清除客户端存储/会话

分别在登录或注销发生时更新用户表的上一次登录日期-时间和注销日期-时间。因此登录日期时间应该总是大于注销(或者如果当前状态是登录且尚未注销,则将注销日期保持为空)

这远比保持额外的黑名单表和定期清洗简单。多设备支持需要额外的表来保存loggedIn、注销日期和一些额外的详细信息,如操作系统或客户端详细信息。

我只是保存令牌到用户表,当用户登录时,我将更新新的令牌,当auth等于用户当前jwt。

我认为这不是最好的解决方案,但对我来说是可行的。

在内存中做一个这样的列表

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

如果您的令牌在一周内到期,则清除或忽略更早的记录。同时只保留每个用户的最新记录。 列表的大小取决于您保留令牌的时间以及用户撤销令牌的频率。 仅当表发生变化时才使用db。当应用程序启动时,在内存中加载表

如果你希望能够撤销用户令牌,你可以跟踪你的DB上所有发出的令牌,并检查它们在一个类似会话的表上是否有效(存在)。 缺点是你将在每一个请求中命中DB

我还没有尝试过,但我建议使用以下方法来允许令牌撤销,同时将DB命中保持在最小值-

为了降低数据库检查率,根据某种确定性关联将所有已发行的JWT令牌分成X组(例如,按用户id的第一个数字分为10组)。

每个JWT令牌将保存组id和令牌创建时创建的时间戳。例如,# EYZ0

服务器将在内存中保存所有组id,每个组都有一个时间戳,指示属于该组的用户的最后一次注销事件是什么时候。 例如,# EYZ0 < / p > 使用具有较旧组时间戳的JWT令牌的请求将被检查有效性(DB hit),如果有效,将发出一个具有新时间戳的新JWT令牌供客户端将来使用。 如果令牌的组时间戳较新,我们相信JWT (No DB hit)

所以- - -

  1. 只有当JWT令牌具有旧的组时间戳时,我们才使用DB验证该令牌,而在用户组中的某个人注销之前,未来的请求才会得到验证。
  2. 我们使用组来限制时间戳更改的数量(比如有一个用户登录和退出,就像没有明天一样——只会影响有限数量的用户,而不是所有人)
  3. 我们限制组的数量,以限制内存中保存的时间戳的数量
  4. 使令牌失效是一件轻而易举的事情——只需将其从会话表中删除,并为用户组生成一个新的时间戳。

如果“注销所有设备”选项是可接受的(在大多数情况下是):

  • 将令牌版本字段添加到用户记录中。
  • 将此字段中的值添加到存储在JWT中的声明中。
  • 每次用户注销时增加版本。
  • 在验证令牌时,将其版本声明与存储在用户记录中的版本进行比较,如果不相同则拒绝。

无论如何,在大多数情况下都需要db访问以获取用户记录,因此这不会给验证过程增加太多开销。与维护黑名单不同,在黑名单中,由于需要使用连接或单独调用、清除旧记录等,数据库负载非常重要。

如果不对每个令牌验证进行DB查找,这似乎很难解决。我能想到的替代方案是在服务器端保留无效令牌的黑名单;当发生更改时,应该在数据库上进行更新,通过使服务器在重新启动时检查数据库以加载当前黑名单来持久化更改。

但是如果你把它放在服务器内存中(一个全局变量),那么如果你使用多个服务器,它就不能跨多个服务器伸缩,所以在这种情况下,你可以把它保存在一个共享的Redis缓存中,应该设置在某个地方持久化数据(数据库?文件系统?),以防它必须重新启动,每次新服务器启动时,它都必须订阅Redis缓存。

作为黑名单的替代方案,使用相同的解决方案,您可以在redis中保存每个会话的散列,正如其他回答指出的那样(虽然不确定在许多用户登录时是否更有效)。

听起来是不是很复杂?对我来说是这样的!

免责声明:我没有使用过Redis。

---------------- 这个答案一点迟到但可能会帮助别人 ----------------

客户端,最简单的方法是从浏览器的存储中删除令牌。

但是,如果您想销毁节点服务器上的令牌-

JWT包的问题是它没有提供任何方法或方法来销毁令牌。 您可以使用上面提到的关于JWT的不同方法。但是这里我用的是jwt-redis

因此,为了销毁服务器端的令牌,您可以使用JWT -redis包代替JWT

这个库(jwt-redis)完全重复了库jsonwebtoken的全部功能,只增加了一个重要的功能。Jwt-redis允许您将tokenIdentifier存储在redis中以验证有效性。redis中缺少tokenIdentifier使得令牌无效。要销毁jwt-redis中的令牌,有一个destroy方法

它是这样工作的:

  1. < p > # EYZ0

  2. < p > # EYZ0

var redis = require('redis');


var JWTR =  require('jwt-redis').default;


var redisClient = redis.createClient();


var jwtr = new JWTR(redisClient);


const secret = 'secret';


const tokenIdentifier = 'test';


const payload = { jti: tokenIdentifier }; // you can put other data in payload as well


jwtr.sign(payload, secret)
.then((token)=>{
// your code
})
.catch((error)=>{
// error handling
});
  1. 验证:
jwtr.verify(token, secret);
  1. 摧毁:
// if jti passed during signing of token then tokenIdentifier else token
jwtr.destroy(tokenIdentifier or token)

# EYZ0:

1).你可以在token的登录过程中提供expiresIn,就像JWT中提供的一样。

2).如果在token的签名过程中没有传递jti,那么jti将由库随机生成。

也许这能帮到你或其他人。谢谢。

如果您正在使用axios或类似的基于承诺的http请求库,您可以简单地在前端.then()部分中销毁令牌。它将在用户执行此函数后在response .then()部分中启动(来自服务器端点的结果代码必须是ok的,200)。用户在搜索数据时单击此路由,如果数据库字段user_enabled为false,将触发销毁令牌,用户将立即注销并停止访问受保护的路由/页面。当用户永久登录时,我们不必等待令牌到期。

function searchForData() {   // front-end js function, user searches for the data
// protected route, token that is sent along http request for verification
var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser


// route will trigger destroying token when user clicks and executes this func
axios.post('/my-data', {headers: {'Authorization': validToken}})
.then((response) => {
// If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
if (response.data.user_enabled === false) {  // user_enabled is field in the db
window.localStorage.clear();  // we destroy token and other credentials
}
});
.catch((e) => {
console.log(e);
});
}

我要回答的是,当我们使用JWT时,我们是否需要提供从所有设备注销功能。这种方法将对每个请求使用数据库查找。因为即使服务器崩溃,我们也需要持久安全状态。在用户表中,我们将有两列

  1. LastValidTime(默认为创建时间)
  2. 登录(默认值:true)

每当有来自用户的注销请求时,我们将LastValidTime更新为当前时间,login - in更新为false。如果有一个登录请求,我们不会改变LastValidTime,但登录将被设置为true。

当我们创建JWT时,我们将在有效载荷中有JWT创建时间。当我们授权一项服务时,我们将检查3个条件

  1. JWT有效吗
  2. JWT有效负载创建时间是否大于用户LastValidTime
  3. 用户是否已登录

让我们来看一个实际的场景。

用户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在一个设备注销后。

IAM解决方案,如Keycloak(我曾经工作过)提供令牌撤销端点

令牌撤销端点 # EYZ0 < / p >

如果只是想注销用户代理(或用户),也可以调用端点(这将使令牌无效)。同样,在Keycloak的情况下,依赖方只需要调用端点

# EYZ0

链接,如果你想了解更多

以下方法可以提供两全其美的解决方案:

让“immediate"平均“~1分钟”。

例:

  1. 用户登录失败

    A.增加“发行时间”;字段转换为令牌,并根据需要保留过期时间。

    < p > B。存储用户密码的哈希值 或者在用户表中创建一个新字段,比如tokenhash。 将tokenhash存储在生成的令牌中
  2. 用户访问url:

    < p >。如果“发行时间”;是在&;immediate&;范围,正常处理令牌。不要更改“发行时间”。取决于“立即”的持续时间;这是持续时间 一个是脆弱的。但像一两分钟这样的短时间就不应该了 风险太大。(这是性能和安全性之间的平衡)。3在这里不需要hit the db .

    B.如果令牌不在“立即”中;范围,检查tokenhash对db。如果没问题,更新“发布时间”;字段。如果不行,那么就不要处理请求(最终强制执行安全性)。

  3. 用户更改tokenhash以确保帐户安全。在&;immediate&;将来帐户是安全的。

我们将数据库查询保存在&;immediate&;的范围内。 如果客户在“立即”中有大量的请求,这是最有益的。时间。< / p >

另一种选择是为关键的API端点提供一个中间件脚本。
如果管理员使令牌失效,此中间件脚本将检入数据库。
此解决方案可能在不需要立即完全阻止用户访问的情况下有用

在本例中,我假设最终用户也有一个帐户。如果不是这样,那么其他的方法也不太可能奏效。

创建JWT时,将其持久化到数据库中,并与正在登录的帐户相关联。这意味着您可以从JWT中提取关于用户的其他信息,因此根据环境的不同,这可能是可行的,也可能是不可行的。

对于之后的每个请求,不仅要执行您所使用的框架附带的标准验证(我希望)(验证JWT是否有效),还包括用户ID或另一个令牌(需要与数据库中的令牌匹配)之类的东西。

注销时,删除cookie(如果使用),并从数据库中使JWT(字符串)无效。如果不能从客户端删除cookie,那么至少注销过程将确保令牌被销毁。

我发现这种方法,加上另一个唯一标识符(数据库中有2个持久化项,可用于前端),会话具有很强的弹性

Kafka消息队列和本地黑名单

我想过使用像kafka这样的消息系统。让我解释一下:

例如,你可以有一个微服务(让我们称之为userMgmtMs服务),它负责loginlogout,并生成JWT令牌。然后将这个令牌传递给客户端。

现在客户端可以使用这个令牌来调用不同的微服务(让我们称它为pricesMs),在pricesMs内,将没有数据库检查users表,从这个表触发了最初的令牌创建。该数据库只能存在于userMgmtMs中。此外,JWT令牌应该包括权限/角色,这样pricesm就不需要从DB中查找任何东西来允许spring安全性工作。

JwtRequestFilter可以提供一个UserDetails对象,该对象由JWT令牌中提供的数据创建(显然没有密码),而不是去到pricesMs中的DB。

那么,如何注销或使令牌失效呢?由于我们不想在每次请求priecesm时都调用userMgmtMs的数据库(这会引入很多不必要的依赖关系),因此解决方案可以使用这个令牌黑名单。

我建议使用kafka消息队列,而不是保持这个黑名单集中,并依赖于所有微服务的一个表。

userMgmtMs仍然负责logout,一旦完成,它就会把它放入自己的黑名单(一个不被微服务共享的表)。此外,它还将带有此令牌内容的kafka事件发送到订阅了所有其他微服务的内部kafka服务。

一旦其他微服务接收到kafka事件,它们也会将其放入内部黑名单。

即使一些微服务在注销时关闭了,它们最终也会重新启动,并在稍后的状态下接收消息。

由于kafka是这样开发的,客户端有他们自己的引用,他们读了哪些消息,确保没有客户端,down或up将错过任何这些无效的令牌。

我能想到的唯一问题是kafka消息传递服务将再次引入单点故障。但它有点相反,因为如果我们有一个全局表,其中所有无效的JWT令牌都保存了,而这个db或micro服务关闭了,那么什么都不能工作。kafka的方法+客户端删除JWT令牌的正常用户注销,kafka停机在大多数情况下甚至不会被注意到。因为黑名单作为内部副本分布在所有微服务中。

在关闭的情况下,你需要使一个被黑客攻击的用户无效,kafka宕机了,这就是问题开始的地方。在这种情况下,作为最后的手段改变秘密可能会有所帮助。或者在这样做之前确保卡夫卡已经起床了。

免责声明:我还没有实现这个解决方案,但不知怎么的,我觉得大多数提议的解决方案都否定了JWT令牌的想法,因为它有一个中央数据库查找。所以我在考虑另一种解决方案。

请让我知道你的想法,它是有意义的还是有一个明显的原因为什么它不能?

使用刷新jwt…

我采用的一种比较实用的方法是在数据库中存储刷新令牌(可以是GUID) 和对应的刷新令牌ID(无论进行多少次刷新都不会改变),在生成用户的JWT时为用户存储将它们添加为声明。可以使用数据库的替代方案,例如内存缓存。但我用的是数据库。

然后,创建一个JWT刷新Web API端点,客户机可以在JWT到期之前调用该端点。当调用刷新时,从JWT中的声明中获取刷新令牌。

在对JWT刷新端点的任何调用中,在数据库上验证当前刷新令牌和刷新令牌ID为一对。生成一个新的刷新令牌,并使用刷新令牌ID替换数据库上的旧刷新令牌。记住它们是可以从JWT中提取出来的声明

从当前JWT中提取用户的声明。开始生成一个新的JWT的过程。将旧的刷新令牌声明的值替换为新生成的刷新令牌,该刷新令牌也是新保存在数据库上的。完成所有这些后,生成新的JWT并将其发送给客户端。

因此,在使用了刷新令牌之后,无论是目标用户还是攻击者,在数据库上使用未与其刷新令牌ID配对的/刷新令牌的任何其他尝试都不会导致生成新的JWT,从而阻止任何拥有该刷新令牌ID的客户端不再能够使用后端,从而导致此类客户端(包括合法客户端)的完全注销。

这解释了基本信息。

接下来要添加的是有一个可以刷新JWT的窗口吗,这样窗口之外的任何事情都是可疑的活动。例如,窗口可以是JWT到期前的10min。生成JWT的日期-时间可以保存为JWT本身的声明。当这种可疑活动发生时,即当其他人试图在窗口外或窗口内重用该刷新令牌ID,而该刷新令牌ID已在窗口内使用时,应该将刷新令牌ID标记为无效。因此,即使是刷新令牌ID的有效所有者也必须重新登录。

如果在数据库上找不到与所提供的刷新令牌ID配对的刷新令牌,则表明刷新令牌ID应该无效。因为空闲用户可能会尝试使用攻击者已经使用过的刷新令牌。

如前所述,在目标用户之前被攻击者窃取和使用的JWT,在用户尝试使用刷新令牌时也会被标记为无效。

唯一没有涉及的情况是,即使攻击者可能已经窃取了JWT,客户端也从未尝试刷新它。但是这种情况不太可能发生在不受攻击者监管(或类似)的客户端上,这意味着攻击者无法预测客户端何时停止使用后端。

如果客户端发起常规注销。应该通过注销从数据库中删除刷新令牌ID和相关记录,从而防止任何客户端生成刷新JWT。

没有使用jwt的刷新…

我想到了两种袭击的场景。一个是关于登录凭证的泄露。另一个是对JWT的盗窃。

对于登录凭据受损,当发生新的登录时,通常向用户发送电子邮件通知。因此,如果客户不同意成为登录的人,应该建议他们重置凭据,这应该将密码最后设置的日期-时间保存到数据库/缓存中(当用户在初始注册期间设置密码时也要设置此时间)。每当用户操作被授权时,应该从数据库/缓存中获取用户更改密码的date-time,并将其与生成给定JWT的date-time进行比较和禁止在证书重置之前生成的jwt的操作,因此基本上使这样的jwt无用。这意味着将生成JWT的日期-时间保存为JWT本身的声明。在ASP。NET Core,一个策略/要求可以用来做这个比较,如果失败,客户端是被禁止的。因此,每当完成凭证重置时,都会在后端全局注销用户。

对JWT的实际盗窃…盗窃JWT不容易被发现,但JWT过期很容易解决这个问题。但是如何在JWT过期之前阻止攻击者呢?它有一个实际的全局注销。它类似于上面描述的凭证重置。为此,通常使用在数据库/缓存中保存用户发起全局注销的日期-时间,并在授权用户操作时,获取它并将其与生成给定JWT的日期-时间进行比较,并禁止在全局注销之前生成的jwt的操作,因此基本上使此类jwt无用。这可以使用ASP中的策略/需求来完成。NET Core,如前所述。

现在,我对这个问题的回答是偶尔提醒用户全局退出并重新登录,因为这肯定会让攻击者退出。

下面是如何做到这一点,而不必对每个请求都调用数据库:

  • 在内存缓存中保留一个有效标记的hashmap(例如一个大小有限的LRU)
  • 当检查令牌时:如果令牌在缓存中,立即返回结果,不需要数据库查询(大多数情况下)。否则执行完整检查(查询数据库,检查用户状态&无效标记……)。然后更新缓存。
  • 当令牌失效时:将其添加到数据库的黑名单中,然后更新缓存,如果需要,向所有服务器发送信号。

请记住,缓存的大小应该是有限的,就像LRU一样,否则可能会耗尽内存。

使令牌失效的好方法仍然需要数据库访问。其目的包括当用户记录的某些部分发生更改时,例如更改角色、更改密码、电子邮件等。您可以在用户记录中添加modifiedupdated_at字段,用于记录更改的时间,然后将其包含在声明中。因此,当JWT经过身份验证时,您将声明中的时间与数据库中记录的时间进行比较,如果声明的时间在此之前,则令牌无效。这种方法也类似于在DB中存储iat

如果您正在使用modifiedupdated_at选项,那么您还必须在用户登录和退出时更新它。

即使从存储中删除令牌,它仍然有效,但只是在短时间内有效,以降低它被恶意使用的可能性。

您可以创建deny-listing,一旦从存储中删除令牌,就可以将令牌添加到这个列表中。如果你有一个微服务,所有其他使用这个令牌的服务都必须添加额外的逻辑来检查这个列表。这将集中您的身份验证,因为每个服务器都必须检查一个集中的数据结构。

简单地创建添加以下对象到你的用户模式:

const userSchema = new mongoose.Schema({
{
... your schema code,
destroyAnyJWTbefore: Date
}

当你在/login收到POST请求时,将这个文档的日期更改为Date.now()

最后,在您的身份验证检查代码中,即在您的中间件中检查isAuthanticatedprotected或任何您使用的名称,只需添加一个检查myjwt.iat大于userDoc.destroyAnyJWTbefore的验证。

  • 如果您想在服务器端销毁JWT,那么在安全性方面,这个解决方案是最好的。
  • 这个解决方案不再依赖于客户端,它打破了使用jwt的主要目标,即停止在服务器端存储令牌。
  • 这取决于您的项目上下文,但最可能的是您想要从服务器上销毁JWT。

如果您只想从客户端销毁令牌,只需从浏览器中删除cookie(如果您的客户端是浏览器),在智能手机或任何其他客户端上也可以这样做。

如果选择从服务器端销毁令牌,我建议您使用Radis通过实现其他用户提到的黑名单样式来快速执行此操作。

天晓得。

我最终使用了访问-刷新令牌,其中刷新令牌uuid存储在数据库中,访问令牌uuid存储在缓存服务器中,作为有效访问令牌的白名单。例如,我对用户数据进行了关键的更改,例如,他的访问权限,接下来我要做的一件事-我将他的访问令牌从缓存服务器白名单中删除,下一次访问我的api的任何资源时,认证服务将被询问令牌的有效性,然后,如果它不在缓存服务器白名单中,我将拒绝用户的访问令牌,并强制他通过刷新令牌重新授权。如果我想删除用户的会话或他的所有会话,我只需从白名单中删除他的所有令牌,并从数据库中删除刷新令牌,因此他必须重新输入凭据来继续访问资源。

我知道,我的身份验证不再是无状态的,但公平地说,我为什么还要无状态的身份验证呢?