使用JWT和NGINX Plus实现身份验证与基于内容的路由

NGINX | 2018年3月01日

NGINX Plus第10版引入了对使用JSON Web Token(JWT,读作“jots”)卸载Web和API服务身份验证的支持。自R10发布以来,我们持续在每个新版本中扩展功能。

从NGINX Plus R14开始,NGINX Plus支持包含嵌套声明和数组数据的JWT。在API网关场景中,NGINX Plus可以使用JWT对请求连接后端服务和API目标的客户端进行身份验证。

有时会有人请我提供一种基本配置,使用NGINX Plus对JWT进行身份验证,然后根据JWT信息做出更高级的负载均衡决策。最直接的解决方案是:身份验证成功时允许访问服务,失败时阻止或重定向连接。

本文提供了一个从头到尾的概念验证演练,介绍如何使用NGINX Plus实现JWT身份验证和基于内容的路由。为涵盖尽可能广泛的应用场景,并尽量减少对JWT相关知识或经验的要求,我编写了这份“JWT入门”演练。即使此前不了解JWT,也可以按照其中的示例和背景说明部署此解决方案。

如果您已有JWT使用经验,或环境中已有JWT,可以跳过前两节,并根据环境调整所提供的NGINX Plus配置片段,开始根据JWT声明数据制定高级负载均衡决策。

前提条件

本文假定您已全新安装NGINX Plus,默认配置文件位于以下位置:

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/default.conf

有关安装和开始使用NGINX Plus的更多信息,请参阅《NGINX Plus管理指南》。

所有CLI命令均假定使用root权限,因此非root用户必须在其环境中拥有sudo权限。

如需了解更多背景信息以及JWT在NGINX Plus中的其他用例,请参阅以下链接:

创建JWT及其关联的签名密钥

以下说明将指导您使用本示例特定的有效负载数据从头创建JWT,以演示如何配置NGINX Plus对JWT声明进行基本处理。如果使用现有JWT而非示例JWT,需要确保“secrets”文件包含与创建JWT时所用签名密钥匹配的Base64URL编码字符串。您可能还需要修改JWT有效负载中的声明。

注意:无论采用何种方式生成JWT,都必须使用Base64URL编码。该编码可正确处理填充(使用=字符)以及Base64编码中通常使用的其他不符合HTTP规范的字符。许多工具会自动处理这些字符,但手动使用的CLI Base64编码器和某些JWT创建工具不会。有关Base64URL编码的更多信息,请参阅Brock Allen撰写的《Base64URL编码》和RFC 4648。

在本例中,我们使用jwt.io上的GUI(可正确进行Base64URL编码)创建对称HS256 JWT。以下屏幕截图显示了按照下方说明输入指定值并验证签名后GUI的界面。

在jwt.io的GUI中,通过核对右侧“Decoded”列各字段中的值或填入指定值,生成HS256 JWT:

  1. 确认以下默认值显示在“HEADER”字段中;如有必要,请修改内容以使其一致:
{  "alg": "HS256",
  "typ": "JWT"
}
  1. 在VERIFY SIGNATURE字段中,将框内的默认值secret替换为nginx123。(请先完成此更改,再在PAYLOAD字段中输入数据,以免颠倒这两个步骤的顺序而导致问题。)
  2. 将PAYLOAD字段的内容替换为以下内容:
{  "exp": 1545091200,
  "name": "Create New User",
  "sub": "cuser",
  "gname": "wheel",
  "guid": "10",
  "fullName": "John Doe",
  "uname": "jdoe",
  "uid": "222",
  "sudo": true,
  "dept": "IT",
  "url": "http://secure.example.com"
}
  1. 注意: exp声明用于设置JWT的到期日期和时间,并以UNIX纪元时间表示(自1970年1月1日UTC零时起经过的秒数)。示例值表示2018年12月18日UTC零时。要调整到期日期,请更改纪元时间。
  2. 确认字段下方的栏为蓝色,并显示“签名已验证”。
  3. 将左侧“Encoded”列中的值复制到文件或缓冲区。该值是用户jdoe访问http://secure.example.com时需要提供的完整JWT文本,我们将在下文的测试中使用该值。为便于显示,此处将JWT分行展示,但向NGINX Plus提供时必须使用单行字符串。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOi
    JDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkI
    joiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMjIy
    Iiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGx
    lLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

在NGINX Plus主机上,按照以下步骤创建密钥文件,供NGINX Plus用于验证使用nginx123签名的JWT:

  1. 运行此命令,生成与签名字符串对应的Base64URL编码字符串。(tr命令会执行Base64URL编码所需的字符替换。)
# echo -n nginx123 | base64 | tr '+/' '-_' | tr -d '='bmdpbngxMjM
  1. 在/etc/nginx/目录中,创建名为api_secret.jwk的密钥文件,供NGINX Plus用于验证JWT签名。插入以下内容。k字段中的值是nginx123经Base64URL编码后的形式,该值已在上一步中生成。
{"keys":    [{
        "k":"bmdpbngxMjM",
        "kty":"oct"
    }]
}

配置NGINX Plus以处理JWT

本节说明如何配置NGINX Plus,以验证请求中包含的JWT,并在客户端获得授权时提供受保护的资源(而不是向未授权客户端显示的默认页面)。此外,我们还将定义一种新的日志格式,用于记录JWT相关信息。

配置JWT验证和基于内容的路由

在这些说明中,我们遵循标准最佳实践,将default.conf配置文件重命名,使NGINX Plus不再读取该文件,并创建专用于测试的新配置。这样,测试完成或测试期间出现问题时,便可轻松恢复默认配置。

  1. 重命名default.conf:
# mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak
  1. 在/etc/nginx/conf.d/中新建名为jwt-test.conf的配置文件,并添加以下内容。这些内容用于配置JWT专用日志记录、JWT验证和基于内容的路由(代码片段后附有完整分析)。
server {
    listen       80;
    access_log  /var/log/nginx/host.access.log jwt;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;

        # JWT validation
        auth_jwt "JWT Test Realm" token=$arg_myjwt;
        auth_jwt_key_file /etc/nginx/api_secret.jwk;
        error_log /var/log/nginx/host.jwt.error.log debug;

        if ( $jwt_claim_uid = 222 ) {
            add_header X-jwt-claim-uid "$jwt_claim_uid" always;
            add_header X-jwt-status "Redirect to $jwt_claim_url" always;
            return 301 $jwt_claim_url;
        }

        if ( $jwt_claim_uid != 222 ) {
            add_header X-jwt-claim-uid "$jwt_claim_uid" always;
            add_header X-jwt-status "Invalid user, no redirect" always;
        }
    }
}

The directives in the location块中的指令用于告知NGINX Plus如何处理包含JWT的HTTP请求。(有关access_log指令所定义的日志记录配置,请参阅下一节。)NGINX Plus将执行以下步骤:

  1. 从请求字符串的myjwt参数中提取JWT(由auth_jwt指令的token参数指定)。
  2. 使用auth_jwt_key-file指令指定的签名密钥(此处为api_secret.jwk)解码JWT。然后按如下方式处理有效载荷(这些操作是JWT处理的固有步骤,没有对应的NGINX Plus指令):
    • 验证JWT尚未过期,即有效负载中由exp声明指定的到期日期尚未过去。
    • 为有效负载中的每个声明创建一个键值对。键名是采用以下格式的变量:$jwt_claim_claim-name(例如,$jwt_claim_uid对应uid声明)。
  3. 将所有错误记录到/var/log/nginx/host.jwt.error.log,日志级别为debug。
  4. 测试$jwt_claim_uid的值是否为222(由两个if指令指定),并向客户端发送相应的响应。JWT中的信息正是以这种方式用于执行基于内容的路由。注意:使用if指令评估变量通常不被视为良好实践,一般建议改用map指令。不过,在这个简单示例中,if指令可以按预期工作。
    • 如果值为222,NGINX Plus会发送响应,将客户端重定向(使用return指令)到JWT的url声明中指定的URL。为便于调试,它会在响应中添加两个标头(使用add_header指令):第一个标头用于捕获uid声明的值,第二个标头用于记录客户端已被重定向。
    • 如果值不是222,NGINX Plus将提供默认索引页面(由同一root块中的index和location指令定义)。同样,为便于调试,它会添加标头,以捕获uid声明的值,并记录客户端未能访问JWT中指定URL的情况。

实际上,此配置仅允许授权用户访问受保护的资源。也就是说,持有有效JWT的用户可以访问JWT中指定的URL,而未持有有效JWT的用户只能访问默认页面。

记录JWT数据

我们通过定义名为“jwt”的日志格式,完成基于内容的路由所需的JWT处理配置。jwt-test.conf中的“access_log”指令会引用此格式,将JWT数据记录到访问日志中。

  1. 将以下log_format指令添加到/etc/nginx/nginx.conf:
log_format jwt '$remote_addr - $remote_user [$time_local] "$request" '               '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
               '$jwt_header_alg $jwt_claim_uid $jwt_claim_url';
  1. 此格式包含本演练中使用的两个JWT声明(uid和url),但也可以使用与声明对应的变量名记录任意JWT声明数据,格式为$jwt_claim_claim‑name。
  2. 保存nginx.conf,然后运行以下命令,测试完整配置(包括新的 jwt-test.conf 文件)是否存在语法错误。请修正报告的所有错误。
# nginx -t
  1. 重新加载NGINX Plus。
# nginx -s reload

测试配置

使用浏览器或curl等CLI工具,可以测试NGINX Plus是否正确验证JWT、对提供该JWT的客户端进行身份验证,并执行基于内容的路由。(如果只测试身份验证和验证,而不测试基于内容的路由,请注释掉jwt-test.conf中的两个if代码块。)

要运行测试,请在请求URL中添加myjwt参数,并提供JWT的完整文本,其中url声明为222。为便于显示,此处同样添加了换行符。

http://example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiO
jE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW
1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9
lIiwidWlkIjoiMjIyIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5le
GFtcGxlLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

以下是对应的curl命令(不含换行符,可按需复制并粘贴):

# curl -v example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMjIyIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGxlLmNvbSJ9.YYQCNvzj17F726QvKoIiuRGeUBl_xAKj62Zvc9xkZb4

由于JWT中的uid声明值为222,因此预期NGINX Plus会显示受限页面http://secure.example.com的内容。

现在进行测试,以验证当JWT中的url声明值不是222时,NGINX Plus不会显示受限页面的内容,而是显示本地服务器的index.html页面,在本例中为http://example.com/index.html。

首先,在jwt.io上生成另一个JWT,其中的uid声明值不是222;本例将其设为111。以下是包含该JWT的请求URL:

http://example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiO
jE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW
1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9
lIiwidWlkIjoiMTExIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5l
eGFtcGxlLmNvbSJ9.Ch9xqsGzB8fRVX-3CBuCxP1Ia3oGKB1OnO6qwi_oBgg

The curl命令如下:

# curl -v example.com/index.html?myjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDUwOTEyMDAsIm5hbWUiOiJDcmVhdGUgTmV3IFVzZXIiLCJzdWIiOiJjdXNlciIsImduYW1lIjoid2hlZWwiLCJndWlkIjoiMTAiLCJmdWxsTmFtZSI6IkpvaG4gRG9lIiwidW5hbWUiOiJqZG9lIiwidWlkIjoiMTExIiwic3VkbyI6dHJ1ZSwiZGVwdCI6IklUIiwidXJsIjoiaHR0cDovL3NlY3VyZS5leGFtcGxlLmNvbSJ9.Ch9xqsGzB8fRVX-3CBuCxP1Ia3oGKB1OnO6qwi_oBgg

在这种情况下,预期NGINX Plus提供以下页面:http://example.com/index.html。

在这两种测试条件下,可以使用标头检查工具(例如curl或某些浏览器提供的开发者工具)来验证新标头X-jwt-claim-uid和X-jwt-status是否已添加到响应中。

如果测试期间遇到任何问题,请检查以下位置的访问日志和错误日志:/var/log/nginx/host.jwt*。错误日志尤其有助于发现验证、访问验证文件等方面的问题。

通过Cookie传递JWT

在基本示例中,NGINX Plus 从请求 URL 的 myjwt 参数中提取 JWT。NGINX Plus 还支持通过 Cookie 传递 JWT(有关详细信息,请参阅 NGINX JWT 参考文档)。在 jwt-test.conf 中,更改 auth_jwt 指令,将 token 参数中的第一个元素由 $arg 改为 $cookie:

auth_jwt "JWT Test Realm" token=$cookie_myjwt;

要在名为“myjwt”的Cookie中提供JWT,可使用以下curl命令:

# curl -v --cookie myjwt=JWT-text example.com/index.html

亲自体验使用JWT的基于内容的路由:立即开始NGINX30天免费试用,或联系我们,讨论您的使用场景。


分享

关于作者

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。