我一直在尝试解决类似的问题。我的用户需要针对他们的每个请求进行身份验证。我一直致力于通过后端应用程序(对JWT令牌的验证)至少对用户进行一次身份验证,但是在那之后,我决定不再需要后端了。
我选择避免要求默认不包含的任何Nginx插件。否则,您可以检查nginx-jwt或Lua脚本,这些可能是不错的解决方案。
身份验证
到目前为止,我已经完成了以下工作:
使用将身份验证委托给Nginx auth_request
。这将调用internal
请求传递到我的后端令牌验证端点的位置。仅此一点根本解决不了处理大量验证的问题。
令牌验证的结果使用proxy_cache_key "$cookie_token";
指令进行缓存。成功验证令牌后,后端会添加一条Cache-Control
指令,告诉Nginx仅将令牌缓存最多5分钟。此时,任何一次通过验证的身份验证令牌都在缓存中,来自同一用户/令牌的后续请求将不再接触身份验证后端!
为了保护我的后端应用程序免遭无效令牌潜在的淹没,当我的后端端点返回401时,我还缓存了拒绝的验证。这些内容仅被缓存了很短的时间,以避免可能用此类请求填充Nginx缓存。
我添加了一些其他改进,例如注销端点通过返回401使令牌无效(该令牌也由Nginx缓存),以便如果用户单击注销,即使令牌未过期也无法使用。
另外,我的Nginx缓存包含每个令牌的关联用户作为JSON对象,这使我不必从数据库中获取该信息;并且还使我免于解密令牌。
关于令牌生存期和刷新令牌
5分钟后,令牌将在缓存中过期,因此将再次查询后端。这是为了确保令牌能够无效,因为用户已注销,令牌已被破坏等。这样的定期重新验证以及在后端的正确实现避免了我不得不使用刷新令牌的情况。
传统上,刷新令牌将用于请求新的访问令牌。它们将存储在您的后端中,并且您将验证是否使用刷新令牌发出对访问令牌的请求,该刷新令牌与该特定用户在数据库中拥有的刷新令牌匹配。如果用户注销,或者令牌遭到破坏,则您将在数据库中删除/禁用刷新令牌,以使使用无效的刷新令牌的下一个新令牌请求将失败。
简而言之,刷新令牌通常具有很长的有效性,并且始终根据后端进行检查。它们用于生成有效性很短(几分钟)的访问令牌。这些访问令牌通常确实会到达您的后端,但是您仅检查它们的签名和有效期。
在这里,在我的设置中,我们使用有效期更长的令牌(可以是数小时或一天),它们的角色和功能与访问令牌和刷新令牌相同。由于Nginx缓存了它们的验证和失效,因此后端每5分钟仅对其进行完全验证。因此,我们保留了使用刷新令牌(能够快速使令牌无效)的好处,而不会增加复杂性。而且,即使仅用于签名和有效期检查,简单验证也永远不会比Nginx缓存慢至少一个数量级的后端到达。
通过此设置,我可以在后端禁用身份验证,因为所有传入请求都auth_request
在触摸Nginx指令之前到达该指令。
如果您需要执行任何类型的每资源授权,那并不能完全解决问题,但是至少您已经保存了基本授权部分。您甚至可以避免解密令牌或进行数据库查找以访问令牌数据,因为Nginx缓存的身份验证响应可以包含数据并将其传递回后端。
现在,我最大的担心是,我可能会打破一些与安全性相关的显而易见的东西而没有意识到。话虽如此,任何接收到的令牌在被Nginx缓存之前仍至少要经过一次验证。任何经过调整的令牌都将不同,因此不会命中缓存,因为缓存密钥也将有所不同。
另外,也许值得一提的是,真实世界的身份验证将通过生成(并验证)额外的Nonce或其他方式来对抗令牌窃取。
这是我的Nginx配置的简化摘录:
# Cache for internal auth checks
proxy_cache_path /usr/local/var/nginx/cache/auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=10m use_temp_path=off;
# Cache for content
proxy_cache_path /usr/local/var/nginx/cache/resx levels=1:2 keys_zone=content_cache:16m max_size=128m inactive=5m use_temp_path=off;
server {
listen 443 ssl http2;
server_name ........;
include /usr/local/etc/nginx/include-auth-internal.conf;
location /api/v1 {
# Auth magic happens here
auth_request /auth;
auth_request_set $user $upstream_http_X_User_Id;
auth_request_set $customer $upstream_http_X_Customer_Id;
auth_request_set $permissions $upstream_http_X_Permissions;
# The backend app, once Nginx has performed internal auth.
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-User-Id $user;
proxy_set_header X-Customer-Id $customer;
proxy_set_header X-Permissions $permissions;
# Cache content
proxy_cache content_cache;
proxy_cache_key "$request_method-$request_uri";
}
location /api/v1/Logout {
auth_request /auth/logout;
}
}
现在,这是内部/auth
端点的配置摘要,上面包括/usr/local/etc/nginx/include-auth-internal.conf
:
# Called before every request to backend
location = /auth {
internal;
proxy_cache auth_cache;
proxy_cache_methods GET HEAD POST;
proxy_cache_key "$cookie_token";
# Valid tokens cache duration is set by backend returning a properly set Cache-Control header
# Invalid tokens are shortly cached to protect backend but not flood Nginx cache
proxy_cache_valid 401 30s;
# Valid tokens are cached for 5 minutes so we can get the backend to re-validate them from time to time
proxy_cache_valid 200 5m;
proxy_pass http://127.0.0.1:1234/auth/_Internal;
proxy_set_header Host ........;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Accept application/json;
}
# To invalidate a not expired token, use a specific backend endpoint.
# Then we cache the token invalid/401 response itself.
location = /auth/logout {
internal;
proxy_cache auth_cache;
proxy_cache_key "$cookie_token";
# Proper caching duration (> token expire date) set by backend, which will override below default duration
proxy_cache_valid 401 30m;
# A Logout requests forces a cache refresh in order to store a 401 where there was previously a valid authorization
proxy_cache_bypass 1;
# This backend endpoint always returns 401, with a cache header set to the expire date of the token
proxy_pass http://127.0.0.1:1234/auth/_Internal/Logout;
proxy_set_header Host ........;
proxy_pass_request_body off;
}
。
处理内容服务
现在,身份验证与数据分离了。由于您告诉过每个用户相同的内容,因此内容本身也可以由Nginx缓存(在我的示例中为content_cache
区域)。
可扩展性
假设您有一台Nginx服务器,此方案开箱即用。在现实世界中,您可能具有高可用性,这意味着多个Nginx实例,还可能托管您的(Laravel)后端应用程序。在这种情况下,您的用户发出的任何请求都可以发送到您的任何Nginx服务器,并且在他们全部本地缓存完令牌之前,他们将一直到达您的后端进行验证。对于少数服务器,使用此解决方案仍会带来很大的好处。
但是,需要注意的是,使用多个Nginx服务器(并因此缓存)时,您将无法在服务器端注销,因为您无法清除(通过强制刷新)所有令牌缓存,例如/auth/logout
在我的例子中。您只剩下500万个令牌缓存持续时间,这将迫使您的后端很快被查询,并将告诉Nginx请求被拒绝。一种解决方法是注销时删除客户端上的令牌头或cookie。
任何评论将非常欢迎和赞赏!