把用户权限存储在JWT声明中,还是在每个请求时在服务器上检查权限哪种更有效?

66

JWT是一种很好的方式,可以确保发送给用户和返回的数据没有被篡改,但这也导致了一些艰难的选择。目前我面临的困境是,在JWT声明中存储授权数据并仅在授权时访问数据库,或仅存储用户ID并在每个请求到达服务器时检查授权级别。

这使选择变得非常困难的原因是该应用程序使用多个授权级别,这使得base64编码的URL非常长和笨重(下面展示了可以预期存储为授权级别的内容)。

另一方面,为了获取授权,需要在数据库中进行两次查找。

所以我的问题是:每个请求额外的开销是否值得避免每个请求时都查找权限所带来的麻烦?

顺便说一句,在权限更改的情况下,查找数据库的方法具有不要求用户重新登录的好处(参见文章)。

"perms": {
    "roles": [
        {
            "name": "Admin",
            "id": 1,
            "assigned": true
        },
        {
            "name": "Webmaster",
            "id": 8,
            "assigned": true
        }
    ],
    "actions": [
        {
            "id": 1,
            "name": "cms-edit",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 9,
            "name": "admin-syslog",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 10,
            "name": "admin-debug",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 12,
            "name": "member-list-extended",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 2,
            "name": "cms-list",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 3,
            "name": "cms-add",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 5,
            "name": "member-list",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 7,
            "name": "member-view",
            "parameters": null,
            "parameterized": null
        },
        {
            "id": 8,
            "name": "member-edit",
            "parameters": null,
            "parameterized": null
        }
    ]
4个回答

47

你的第一个问题:

发送权限到服务器上每个请求所带来的额外开销是否值得避免每个请求时查找权限的麻烦?

答案:

让我们看一下jwt.io提供的关于何时使用JWT的描述:

授权:这是使用JWT最常见的场景。一旦用户登录,每个后续请求将包括JWT,允许用户访问使用该令牌许可的路由、服务和资源。单点登录是一项广泛使用JWT的功能,因为它具有小的开销并且易于在不同域之间使用。

这意味着您需要在用户登录后在服务器端生成令牌。

它包含以下内容:

  • 用户(作为ID或名称)
  • 客户端拥有的角色(用户、管理员、访客等等...)

一旦客户端向服务器请求或发送数据,服务器首先检查给定的令牌是否有效和已知,然后检查角色是否符合访问某个资源的条件。

所有角色/访问数据都可以在系统启动时读取并保存在内存中。此外,客户端拥有的角色只有在客户端登录时才会从数据库中读取一次。这样,您就不需要后续的数据库访问,从而大大提高了性能。

另一方面,如果客户端请求数据或想执行操作,则需要一个身份验证机制,该机制评估传递的令牌是否具有所需的角色以执行此操作。

那样我们解决了数据库的麻烦,同时也消除了向客户端暴露过多信息的危险(即使客户端无法篡改数据,它也可以读取数据!)
注意:https://jwt.io/introduction 请注意,使用签名令牌时,令牌中包含的所有信息都会暴露给用户或其他方,尽管他们无法更改它。这意味着您不应在令牌中放置机密信息。
参见A3(敏感数据泄露):https://www.owasp.org/index.php/Top_10-2017_Top_10 最后:如果客户端长时间处于闲置状态或有意注销,请同时使令牌失效。
后续问题:
权限更改的情况下,查找数据库的方法具有无需用户重新登录的好处。
答案:
根据您的服务器基础架构,您可以编写刷新机制(如果角色更新,则服务器生成新令牌并将其与生成的答案一起发送到客户端,使旧令牌失效,客户端仅使用最近的令牌并覆盖旧令牌),或在服务器端添加一些状态,例如客户端会话。

消除令牌中的角色/权限。最好为客户端生成会话并在服务器端提供会话角色/权限。客户端获取会话令牌(通常是ID),可以用它进行身份验证。一旦权限/角色更改,我们必须执行两个操作:

  1. 更新数据库
  2. 更新会话的角色/权限

同样,每个后续请求都将在内存中进行角色/权限检查,并且不需要数据库通信,而客户端只有一个小型会话令牌(或您的JWT)。因此,角色/权限更改对客户端透明(无需重新登录),并且我们消除了JWT刷新要求。


1
根据我的经验,如果所有系统都使用某个中央角色和权限数据库,那么您可以将所有信息添加到JWT中。然而,在SSO情境下,当身份验证服务器本身对接收并信任令牌的目标系统一无所知时,这种方法可能不起作用。特别是当您将SSO身份验证与JWT集成到已有许可子系统的一些传统系统中时,它就更为真实,因此它们只需要JWT中存在一个声明 - 用户身份的声明。 - JustAMartin
1
你能再详细解释一下吗?我认为“JWT”的发明初衷就是为了“移除会话”,因为会话存在内存限制和分布限制...所以将JWT放入其中是唯一的选择?然后我通常看到的选项是回到会话...我真的很想理解。虽然我从来没有找到过足够详细的答案...文章通常只说利弊,但并没有说明问题是如何解决的。动态权限的自定义角色,如何实现。使用哪种方法以及为什么这种方法比另一种更好,如果整个目的是为了摆脱会话的限制。 - Seabizkit
2
作为具有微服务架构的系统:存在相互通信的小型隔离服务。如何知道会话是否有效?每个服务都必须每次与中央实例通信。想象一下所产生的开销。使用JWT:您可以信任的所有内容都在该JWT中。由于只有第一个服务需要进行更新/使无效和获取操作,因此您可以消除这种麻烦。其他服务只需检查签名是否有效即可。角色应该在哪里实现?在需要特定权限的每个服务中。从哪里获取所有角色?第一个服务与中央身份验证服务通信。 - Akar
25
我惊讶地看到这个答案被接受了,因为它没有得出任何关于如何管理角色权限的结论。角色权限是如何在内存中完成的,或者更新如何在不同的服务之间传播?这个回答非常令人困惑且不完整。 - Zahan Safallwa
1
@ZahanSafallwa,您的所有授权将集中在一个服务中,该服务返回一个包含允许权限的令牌。然后客户端可以使用该令牌访问不同的终端点。这些终端点可以将所需的权限存储为硬编码或从具有“定义”的共享包中提取。您将能够在每个发布版本中推出/更改权限。希望这样说得清楚明白。 - David S
请注意,对于JWT的使用,如果您完全依赖它们进行授权,那么它们确实需要是短期的,并且需要使用刷新令牌来保证合理的安全性。如果您将它们视为无需服务器状态/数据库查找的承载令牌,则用户在JWT有效期内可能会一直拥有访问权限。 - JCP

10

这取决于您如何解释效率。在谈论资源效率时,请记住每个请求都会传输您的JWT。因此,如果您有一个具有细粒度访问控制列表(ACL)的大型应用程序,并且您总是在每个请求-响应上来回传递这些列表,那么这肯定会耗费一些千字节,具体取决于您发出的请求数量。


1

我也遇到了同样的问题。

我只在 Jwt 令牌中存储用户角色以进行第一次权限检查。 因此,要检查用户是否允许进行 post、get 等请求。

对于更多的权限,我使用 casl 来检查用户可以访问哪些字段。


0

这是一个自从我开始构建Web应用程序以来一直让我关注的话题。过去,我也曾将所有授权(authZ)信息与身份验证(authN)信息一起编码到JWT中。然而,越来越多地阅读它,我开始怀疑这种方法。现代身份验证是使用一些集中式身份验证系统完成的,用户可以从中受益于SSO。无论您使用哪个提供商(Google、Microsoft等),他们都不关心目标系统,因此如何实现authZ。因此,我认为不应将authZ信息添加到令牌中。带有用户身份的JWT被发送到目标系统,该系统应验证用户被允许访问什么。

好文章:

https://blog.joaograssi.com/posts/2021/asp-net-core-permission-based-authorization-middleware/

https://sdoxsee.github.io/blog/2020/01/06/stop-overloading-jwts-with-permission-claims


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接