博客 | NGINX

使用 JWT 和 NGINX Plus 对 API 客户端进行身份验证

NGINX-F5-horiz-black-type-RGB 的一部分
Liam Crilly 缩略图
利亚姆·克里利
2021 年 12 月 02 日发布

JSON Web Tokens(JWT,发音为“jots”)是一种紧凑且高度可移植的身份信息交换方式。 JWT 规范一直是OpenID Connect的重要基础,为 OAuth 2.0 生态系统提供单点登录令牌。 JWT 本身也可以用作身份验证凭证,并且比传统 API 密钥更能控制对基于 Web 的 API 的访问。

NGINX Plus R10 及更高版本可以直接验证 JWT。 在这篇博文中,我们描述了如何使用 NGINX Plus 作为 API 网关,为 API 端点提供前端,并使用 JWT 来验证客户端应用。

原生 JWT 支持仅在 NGINX Plus 中提供,而不在 NGINX Open Source 中提供。

编辑器– 这篇博文于 2021 年 12 月更新,使用NGINX Plus R25 中引入的auth_jwt_require指令。 有关该指令的详细讨论,请参阅宣布NGINX Plus R25的博客中的自定义 JWT 验证规则

NGINX Plus R15及更高版本还可以控制 OpenID Connect 1.0 中的“授权码流”,从而实现与大多数主要身份提供商的集成。 有关详细信息,请参阅宣布 NGINX Plus R15<.htmlspan>

JWT 的剖析

JWT 有三个部分:标头、有效负载和签名。 在传输中它们看起来如下所示。 我们添加了换行符以提高可读性(实际的 JWT 是一个字符串)并添加了颜色编码以区分这三个部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0 = .VGYHWPterIaLjRi0LywgN3jnDUQbSsFptUw99g2slfc

如图所示,句点 ( . ) 分隔标头、有效负载和签名。 标头和有效负载是Base64 编码的JSON 对象。 签名使用alg标头指定的算法进行加密,我们可以在解码示例 JWT 时看到这一点:

  编码 已解码
标头 eyJhbGciOiJIUzI1NiIsInR5cCI6Ik
重组质粒
{
“alg”: “HS256”,
“类型”: “智威汤逊”
}
有效载荷 ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0= {
“sub”:“lc1”,
“电子邮件”:“liam.crilly@nginx.com”,
}

JWT 标准定义了几种签名算法。 我们示例中的值HS256指的是 HMAC SHA-256,我们在本博文中将其用于所有示例 JWT。 NGINX Plus 支持标准中定义的HS xxxRS xxxES xxx签名算法。 对 JWT 进行加密签名的能力使其非常适合用作身份验证凭证。

JWT 作为 API 密钥

验证 API 客户端(请求 API 资源的远程软件客户端)的常用方法是通过共享密钥(通常称为API 密钥)。 传统的 API 密钥本质上是一个长而复杂的密码,客户端在每次请求时将其作为附加 HTTP 标头发送。 如果提供的 API 密钥在有效密钥列表中,则 API 端点将授予对所请求资源的访问权限。 通常,API 端点本身不会验证 API 密钥;而是由 API 网关处理身份验证过程并将每个请求路由到适当的端点。 除了计算卸载之外,这还提供了反向代理带来的好处,例如高可用性和对多个 API 端点的负载均衡。

使用传统 API 密钥的 API 客户端和 JWT 身份验证
API 网关在传递请求之前通过查阅密钥注册表来验证 API 密钥
到 API 端点

对不同的 API 客户端应用不同的访问控制和策略是很常见的。 对于传统的 API 密钥,这需要查找以将 API 密钥与一组属性进行匹配。 对每个请求执行此查找会对系统的整体延迟产生可理解的影响。 使用 JWT,这些属性是嵌入的,无需单独查找。

使用 JWT 作为 API 密钥可以提供一种比传统 API 密钥更高性能的替代方案,将最佳实践的身份验证技术与基于标准的交换身份属性的模式相结合。

使用 JWT 和 NGINX Plus 的 API 客户端和 JWT 身份验证
NGINX Plus 在将请求传递到 API 端点之前验证 JWT

将 NGINX Plus 配置为身份验证 API 网关

用于验证 JWT 的 NGINX Plus 配置非常简单。

上游 api_server {
服务器 10.0.0.1;
服务器 10.0.0.2;
}

服务器 {
listen 80;

location /products/ {
auth_jwt "产品 API";
auth_jwt_key_file conf/api_secret.jwk;
proxy_pass http://api_server;
}
}

我们做的第一件事是在上游块中指定托管 API 端点的服务器的地址。 位置块指定任何以/products/开头的 URL 请求都必须经过身份验证。 auth_jwt指令定义将返回的身份验证领域(以及401如果身份验证不成功,则返回状态代码。

auth_jwt_key_file指令告诉 NGINX Plus 如何验证 JWT 的签名元素。 在这个例子中,我们使用 HMAC SHA-256 算法来签署 JWT,因此我们需要在conf/api_secret.jwk中创建一个 JSON Web Key 来包含用于签名的对称密钥。 该文件必须遵循JSON Web Key 规范所描述的格式;我们的示例如下所示:

{"keys":
[{
"k":"ZmFudGFzdGljand0",
"kty":"oct",
"kid":"0001"
}]
}

对称密钥在k字段中定义,这里是明文字符串fantasticjwtBase64URL 编码值。 我们通过运行以下命令获取编码值:

$ echo -n fantasticjwt | base64 | tr'+/''-_'| tr -d'='

kty字段将密钥类型定义为对称密钥(八位字节序列)。 最后, kid (Key ID)字段为这个JSON Web Key定义一个序列号,如下0001,它允许我们在同一个文件中支持多个密钥(由auth_jwt_key_file指令命名)并管理这些密钥和用它们签名的 JWT 的生命周期。

现在我们准备向我们的 API 客户端发布 JWT。

向 API 客户端颁发 JWT

作为示例 API 客户端,我们将使用“报价系统”应用并为 API 客户端创建 JWT。 首先我们定义 JWT 标头:

{
“type”:“JWT”,
“alg”:“HS256”,
“kid”:“0001”
}

typ字段定义类型为 JSON Web Token, alg字段指定 JWT 使用 HMAC SHA256 算法签名, kid字段指定 JWT 使用具有该序列号的 JSON Web Key 签名。

接下来我们定义 JWT 负载:

{
“name”:“报价系统”,
“sub”:“quotes”,
“iss”:“我的 API 网关”
}

(主题)字段是名称字段中完整值的唯一标识符。 iss字段描述了 JWT 的发行者,如果您的 API 网关还接受来自第三方发行者或集中式身份管理系统的 JWT,则该字段很有用。

现在我们已经拥有创建 JWT 所需的一切,我们按照以下步骤正确地对其进行编码和签名。 命令和编码值出现在多行上仅仅是为了便于阅读;实际上每个命令和编码值都被输入为或出现在一行上。

  1. 分别展平标头和有效负载并进行 Base64URL 编码。

     

    $ echo -n '{"typ":"JWT","alg":"HS256","kid":"0001"}' | base64 | tr '+/' '-_' | tr -d '=' eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ $ echo -n '{"name":"报价系统","sub":"quotes","iss":"我的 API 网关"}' | base64 | tr '+/' '-_' | tr -d '=' eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik 15IEFQSSBHYXRld2F5In0
    
  2. 将编码的标头和有效负载用句点 (.) 连接起来,并将结果分配给HEADER_PAYLOAD变量。

    $ HEADER_PAYLOAD = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsIm lzcyI6Ik15IEFQSSBHYXRld2F5In0
    
  3. 使用我们的对称密钥对标头和有效负载进行签名,并使用 Base64URL 编码对签名进行编码。

    $ echo -n $HEADER_PAYLOAD | openssl dgst -binary -sha256 -hmac fantasticjwt | base64 | tr'+/''-_'| tr -d'=' ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
    
  4. 将编码的签名附加到标题和有效负载。

    $ echo $HEADER_PAYLOAD.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I > quotes.jwt
    
  5. 通过向 API 网关发出经过身份验证的请求进行测试(在此示例中,网关在本地主机上运行)。

    $ curl -H“授权: 承载者 `cat quotes.jwt`” http://localhost/products/widget1
    

步骤 5 中的curl命令以Bearer Token的形式将 JWT 发送给 NGINX Plus,这是 NGINX Plus 默认期望的。 NGINX Plus 还可以从 cookie 或查询字符串参数中获取 JWT;要配置此功能,请将token=参数包含在auth_jwt指令中。 例如,通过以下配置,NGINX Plus 可以验证使用此curl命令发送的 JWT:

$ curl http://localhost/products/widget1?apijwt=`cat quotes.jwt`
服务器 { 监听 80; 位置 /products/ { auth_jwt “产品 API”令牌 = $arg_apijwt ; auth_jwt_key_file conf/api_secret.jwk; proxy_pass http://api_server; } }

一旦您配置了 NGINX Plus,并生成并验证了 JWT(如上所示),您就可以将 JWT 发送给 API 客户端开发人员,并同意在每次 API 请求中提交 JWT 的机制。

利用 JWT 声明进行日志记录和速率限制

JWT 作为身份验证凭证的主要优势之一是它们传达“声明”,代表与 JWT 及其有效负载相关的实体(例如,其发行者、发行给的用户以及预期的接收者)。 验证 JWT 之后,NGINX Plus 可以访问标头和有效负载中的所有字段作为变量。 通过将$jwt_header_$jwt_claim_添加到所需字段来访问它们(例如, $jwt_claim_sub表示声明)。 这意味着我们可以非常轻松地将 JWT 中包含的信息代理到 API 端点,而无需在 API 本身中实现 JWT 处理。

此配置示例展示了一些高级功能。

log_format jwt '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent" ' '$jwt_header_alg $jwt_claim_sub' ; limit_req_zone $jwt_claim_sub zone=10rps_per_client:1m rate=10r/s;服务器 { 监听 80; 位置 /products/ { auth_jwt "产品 API"; auth_jwt_key_file conf/api_secret.jwk; limit_req zone=10rps_per_client; proxy_pass http://api_server; proxy_set_header API-客户端 $jwt_claim_sub; access_log /var/log/nginx/access_jwt.log jwt; } }

log_format指令定义了一种名为jwt的新格式,它通过两个附加字段$jwt_header_alg$jwt_claim_sub扩展了通用日志格式。 在位置块内,我们使用access_log指令将从经过验证的 JWT 获取的值写入日志。

在此示例中,我们还使用基于声明的变量来按 API 客户端(而不是按 IP 地址)提供 API 速率限制。 当多个 API 客户端嵌入在单个门户中且无法通过 IP 地址区分时,这特别有用。 limit_req_zone指令使用 JWT sub声明作为计算速率限制的关键,然后通过包含limit_req指令将其应用于位置块。

最后,当请求代理到 API 端点时,我们将 JWT 主题作为新的 HTTP 标头提供。 proxy_set_header指令添加了一个名为API‑Client的 HTTP 标头,API 端点可以轻松使用它。 因此 API 端点不需要实现任何 JWT 处理逻辑。 随着 API 端点数量的增加,这变得越来越有价值。

撤销 JWT

有时可能需要撤销或重新发布 API 客户端的 JWT。 通过将简单的映射块与auth_jwt_require指令相结合,我们可以通过将 API 客户端的 JWT 标记为无效来拒绝访问,直到达到 JWT 的到期日期(在exp声明中表示),此时可以安全地删除该 JWT 的映射条目。

在此示例中,我们将$jwt_status变量设置为0或者1根据令牌中的声明的值(如$jwt_claim_sub变量中捕获的)。 然后,我们使用位置块中的auth_jwt_require指令来额外验证(或拒绝)令牌。 为了有效, $jwt_status变量不能为空,并且不能等于0(零)

映射 $jwt_claim_sub $jwt_status { “引号” 0; “测试” 0; 默认 1; } 服务器 { 监听 80; 位置 /products/ { auth_jwt “产品 API”; auth_jwt_key_file conf/api_secret.jwk; auth_jwt_require $jwt_status; proxy_pass http://api_server; } }

概括

JSON Web Tokens 非常适合提供对 API 的经过身份验证的访问。 对于 API 客户端开发人员来说,它们与传统 API 密钥一样易于处理,并且它们为 API 网关提供身份信息,否则需要数据库查找。 NGINX Plus 提供对 JWT 身份验证的支持,并根据 JWT 本身包含的信息提供复杂的配置解决方案。 结合其他API 网关功能,NGINX Plus 使您能够快速、可靠、可扩展且安全地提供基于 API 的服务。

要亲自尝试使用 NGINX Plus 的 JWT,请立即开始30 天免费试用,或联系我们讨论您的用例


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”