
API调用有多种身份验证方式,包括X.509客户端证书和HTTP基本身份验证。不过,近年来,OAuth 2.0访问令牌已成为事实标准。它们是从客户端传递到API服务器的身份验证凭据,通常通过HTTP标头传输。
然而,OAuth 2.0由一系列相互关联的标准组成,较为复杂。OAuth 2.0身份验证流程中的令牌颁发、出示和验证过程通常依赖多个相关标准。截至本文撰写时,共有8项OAuth 2.0标准。访问令牌就是一个典型例子,因为OAuth 2.0核心规范(RFC 6749)并未规定访问令牌的格式。在实际应用中,常用的格式有两种:
- 由RFC 7519 定义的 JSON Web Token(JWT)
- 不透明令牌,基本上只是已通过身份验证的客户端的唯一标识符
身份验证后,客户端会在每个HTTP请求中提供其访问令牌,以访问受保护的资源。必须验证访问令牌,确保该令牌确实由可信身份提供商(IdP)签发且尚未过期。由于IdP会使用加密方式对其签发的JWT进行签名,因此无需在运行时依赖IdP,即可“离线”验证JWT。通常,JWT还包含可供检查的到期日期。NGINX Plus auth_jwt模块可执行离线JWT验证。
相比之下,不透明令牌必须发回签发该令牌的身份提供商(IdP)进行验证。不过,这样做的优势在于,IdP可以撤销此类令牌,例如在执行全局注销时,避免此前已登录的会话仍处于活动状态。全局注销也可能需要通过IdP验证JWT。
本博客介绍NGINX和NGINX Plus如何充当OAuth 2.0依赖方,将访问令牌发送到IdP进行验证,并且仅代理通过验证的请求。我们将探讨使用NGINX和NGINX Plus执行此任务的多项优势,以及如何通过短时间缓存验证响应来改善用户体验。对于NGINX Plus,我们还将介绍如何使用JavaScript模块更新键值存储,从而在NGINX Plus实例集群中分布缓存。此功能在NGINX Plus R18中引入。
除非另有说明,本博客中的信息同时适用于NGINX Open Source和NGINX Plus。文中提及NGINX Plus时,仅适用于该产品。
令牌内省
使用IdP验证访问令牌的标准方法称为令牌内省。RFC 7662,即《OAuth 2.0令牌内省》,是一项目前已获广泛支持的标准。该标准定义了一个JSON/REST接口,依赖方可通过此接口向IdP提交令牌,同时还规定了响应的结构。许多主流IdP供应商和云服务提供商均支持该标准。
无论使用哪种令牌格式,在每个后端服务或应用程序中执行验证都会产生大量重复代码和不必要的处理。需要考虑各种错误情况和边界情况,而在每个后端服务中分别处理这些情况,很容易导致实现不一致,进而造成不可预测的用户体验。请思考每个后端服务可能会如何处理以下错误情况:
- 缺少访问令牌
- 超大访问令牌
- 访问令牌中包含无效或意外字符
- 提供了多个访问令牌
- 后端服务之间的时钟偏差

使用NGINX auth_request模块验证令牌
为避免代码重复及其引发的问题,可以使用NGINX代替后端服务验证访问令牌。这样做有以下几项优势:
- 只有当客户端提供有效令牌时,请求才会到达后端服务
- 无需更改代码,即可使用访问令牌保护现有后端服务
- 只需向IdP注册NGINX实例,无需注册每个应用
- 所有错误情况下的行为均保持一致,包括令牌缺失或无效的情况

当NGINX充当一个或多个应用的反向代理时,可以使用auth_request模块,在将请求代理到后端之前触发对IdP的API调用。稍后将会看到,以下解决方案存在一个根本缺陷,但它介绍了auth_request模块的基本工作原理,后续章节将在此基础上进一步展开。
正在加载代码示例…
第5行的“auth_request”指令指定了处理API调用的位置。仅当“auth_request”响应成功时,才会代理到后端(第6行)。“auth_request”位置在第9行定义,并标记为“internal”,以防止外部客户端直接访问。
第11至14行定义了请求的各项属性,使其符合令牌内省请求格式。请注意,内省请求中发送的访问令牌是第14行所定义请求正文的一部分。此处,token=$http_apikey表示客户端必须在apikey请求标头中提供访问令牌。当然,也可以通过请求的任何属性提供访问令牌,在这种情况下,需要使用其他NGINX变量。
使用NGINX JavaScript模块扩展auth_request with the NGINX JavaScript Module
如前所述,以这种方式使用auth_request模块并不能提供完整的解决方案。auth_request模块使用HTTP状态码来判断是否成功(2xx表示成功,4xx表示失败)。但是,OAuth 2.0令牌内省响应会在JSON对象中对成功或失败进行编码,并且在这两种情况下都会返回HTTP状态码200 (OK)。

我们需要一个JSON解析器,将IdP的内省响应转换为适当的HTTP状态码,以便auth_request模块能够正确解读该响应。
好在,对于NGINX JavaScript模块(njs)而言,解析JSON非常简单。因此,我们不再定义location块来执行令牌内省请求,而是让auth_request模块调用JavaScript函数。
[编辑:本文是探讨NGINX JavaScript模块用例的系列文章之一。完整列表请参阅《NGINX JavaScript模块用例》。
本节代码已更新为使用js_import指令,该指令在js_include指令中取代了NGINX Plus R23及更高版本。有关详细信息,请参阅NGINX JavaScript模块的参考文档,其中的配置示例部分展示了NGINX配置文件和JavaScript文件的正确语法。]
注意:此解决方案要求使用load_module指令在nginx.conf中将JavaScript模块作为动态模块加载。有关操作说明,请参阅《NGINX Plus 管理指南》。
正在加载代码示例…
第13行的“js_content”指令将JavaScript函数“introspectAccessToken”指定为“auth_request”处理程序。该处理程序函数在“oauth2.js”中定义:
正在加载代码示例…
请注意,introspectAccessToken函数会向下方配置片段中定义的另一个位置(第2行)发出HTTP子请求(/oauth2_send_request)。随后,JavaScript代码会解析响应(第5行),并根据auth_request字段的值,将相应的状态码返回给active模块。有效(处于活动状态)的令牌返回HTTP 204 (No Content)(表示成功),无效令牌则返回HTTP 403 (Forbidden)。发生错误时返回HTTP 401 (Unauthorized),以便区分错误和无效令牌。
注意:此代码仅用于概念验证,尚未达到生产环境要求。下文提供了包含全面错误处理和日志记录功能的完整解决方案。
第2行定义的子请求目标位置与原始的auth_request配置非常相似。
正在加载代码示例…
用于构造令牌内省请求的所有配置均包含在/_oauth2_send_request位置中。通常只需配置身份验证(第19行)、访问令牌本身(第21行)以及令牌内省端点的URL(第22行)。必须进行身份验证,IdP才会接受来自此NGINX实例的令牌内省请求。《OAuth 2.0令牌内省》规范要求进行身份验证,但未指定具体方法。在此示例中,我们在Authorization标头中使用不记名令牌。
完成此配置后,当NGINX收到请求时,会将其传递给JavaScript模块,由该模块向IdP发出令牌内省请求。系统会检查IdP的响应,当active字段为true时,即视为身份验证成功。此解决方案可通过NGINX简洁高效地执行OAuth 2.0令牌内省,并且可以轻松调整以适配其他身份验证API。
但这还没有结束。一般来说,令牌内省面临的主要挑战是会增加每个HTTP请求的延迟。如果相关IdP是托管解决方案或云提供商,这可能会成为一个严重问题。NGINX和NGINX Plus可以通过缓存内省响应来缓解这一问题。
优化 1:使用 NGINX 缓存
IdP通过JSON/REST端点提供OAuth 2.0令牌内省,因此标准响应是HTTP状态码为200的JSON正文。以访问令牌作为此响应的缓存键后,该响应非常适合缓存。

可以将NGINX配置为缓存每个访问令牌的内省响应副本。下次收到同一访问令牌时,NGINX会提供缓存的内省响应,而不是向IdP发出API调用。这可显著降低后续请求的总体延迟。可以控制缓存响应的使用时长,以降低接受已过期或最近被撤销的访问令牌的风险。例如,如果API客户端通常会在短时间内连续发出多次API调用,那么将缓存有效期设为10秒或许足以显著改善用户体验。
通过指定缓存的存储位置即可启用缓存:使用磁盘目录存储缓存内容(内省响应),并使用共享内存区存储键(访问令牌)。
正在加载代码示例…
The proxy_cache_path指令会分配所需的存储空间:/var/cache/nginx/oauth用于存储内省响应,并分配一个名为token_responses的内存区域用于存储键。该指令在http上下文中配置,因此位于server和location块之外。随后,在处理令牌内省响应的location块中启用缓存:
正在加载代码示例…
此位置已通过proxy_cache指令启用缓存(第26行)。默认情况下,NGINX基于URI进行缓存,但在本例中,我们希望根据apikey请求标头中提供的访问令牌来缓存响应(第27行)。
在第28行,我们使用proxy_cache_lock指令告知NGINX:如果使用相同缓存键的并发请求到达,则需要等待第一个请求填充缓存后,再响应其他请求。proxy_cache_valid指令(第29行)用于告知NGINX内省响应的缓存时长。如果没有此指令,NGINX会根据IdP发送的缓存控制标头确定缓存时长;但这些标头并不总是可靠,因此我们还会告知NGINX忽略标头,否则这些标头会影响响应的缓存方式(第30行)。
启用缓存后,出示访问令牌的客户端只需每10秒承担一次令牌内省请求的延迟开销。
优化 2:使用 NGINX Plus 实现分布式缓存
将内容缓存与令牌内省相结合,可以显著提升应用的整体性能,同时对安全性的影响微乎其微。但是,如果以分布式方式部署NGINX,例如跨多个数据中心、云平台或双活集群进行部署,则缓存的令牌内省响应仅可供发起内省请求的NGINX实例使用。
借助NGINX Plus,可以使用keyval模块(一种内存键值存储)缓存令牌内省响应。此外,还可以使用zone_sync模块在NGINX Plus实例集群中同步这些响应。这意味着,无论由哪个NGINX Plus实例执行令牌内省请求,集群中的所有NGINX Plus实例都可以获取该响应。
注意:本文不涉及用于共享运行时状态的zone_sync模块配置。有关在NGINX Plus集群中共享状态的更多信息,请参阅《NGINX Plus管理指南》。
在NGINX Plus R18及更高版本中,可以通过修改在keyval指令中声明的变量来更新键值存储。由于JavaScript模块可以访问所有NGINX变量,因此可在处理响应时将内省响应填充到键值存储中。
与NGINX文件系统缓存类似,指定存储空间即可启用键值存储。在本例中,存储空间是一个内存区域,用于存储键(访问令牌)和值(内省响应)。
正在加载代码示例…
请注意,通过timeout参数为keyval_zone指令指定的缓存响应有效期与auth_request_cache.conf第29行相同,均为10秒,因此NGINX Plus集群的每个成员都会在响应过期时独立将其删除。第2行指定了每个条目的键值对:键是通过apikey请求标头提供的访问令牌,值是由$token_data变量求值的内省响应。
现在,对于每个包含apikey请求标头的请求,如果存在之前的令牌内省响应,则会将其赋值给$token_data变量。因此,我们更新JavaScript代码,以检查是否已有令牌内省响应。
正在加载代码示例…
第2行检查键值存储中是否已存在此访问令牌的条目。由于可通过两种途径获取内省响应(从键值存储中获取,或从内省响应中获取),因此我们将验证逻辑移至以下单独的函数中:tokenResult:
正在加载代码示例…
现在,每个令牌内省响应都会保存到键值存储中,并同步到NGINX Plus集群的所有其他成员。以下示例先展示一个包含有效访问令牌的简单HTTP请求,然后查询NGINX Plus API,以显示键值存储中的内容。
$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/HTTP/1.1 200 OK
Date: Wed, 24 Apr 2019 17:41:34 GMT
Content-Type: application/json
Content-Length: 612
$ curl http://localhost/api/4/http/keyvals/access_tokens
{"tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA":"{\"active\":true}"}请注意,键值存储本身使用JSON格式,因此令牌内省响应中的引号会自动进行转义。
优化 3:从内省响应中提取属性
OAuth 2.0令牌内省的一项实用功能是,响应中除了令牌的活动状态外,还可以包含令牌相关信息。此类信息包括令牌到期日期以及关联用户的属性,例如用户名、电子邮件地址等。

这些附加信息非常有用。可以记录这些信息,用于实施精细的访问控制策略,或将其提供给后端应用程序。我们可以在成功的(HTTP auth_request 模块导出这些属性,具体方法是将其作为附加响应标头随成功的(HTTP 204)响应一起发送。
正在加载代码示例…
我们遍历内省响应中的每个属性(第23行),并将其作为响应标头发送回auth_request模块。每个标头名称都以Token-为前缀,以避免与标准响应标头冲突(第26行)。现在,可以将这些响应标头转换为NGINX变量,并用于常规配置。
正在加载代码示例…
在此示例中,我们将username属性转换为新变量$username(第11行)。auth_request_set指令可将令牌内省响应的上下文导出到当前请求的上下文中。每个属性的响应标头(由JavaScript代码添加)均可通过$sent_http_token_attribute访问。随后,第12行将$username的值作为请求标头添加到代理至后端的请求中。对于令牌内省响应返回的任何属性,都可以重复使用此配置。

生产环境配置
上述代码和配置示例均可正常运行,适用于概念验证测试,也可针对特定用例进行定制。对于生产环境,强烈建议增加错误处理和日志记录,并采用灵活的配置。您可以在我们的GitHub仓库中找到适用于NGINX和NGINX Plus的更稳健、更详尽的实现:
摘要
本文介绍了如何结合使用NGINX auth_request模块和JavaScript模块,对客户端请求执行OAuth 2.0令牌内省。此外,我们还通过缓存扩展了该解决方案,并从内省响应中提取属性,供NGINX配置使用。
我们还介绍了如何将NGINX Plus键值存储用作内省响应的分布式缓存,适用于由多个NGINX Plus实例组成的集群中的生产部署。
亲自体验使用NGINX Plus进行OAuth 2.0令牌内省,立即开始30天免费试用,或联系我们,讨论您的用例。
关于作者

相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。
