BLOG | NGINX

借助 JWT 和 NGINX Plus 验证 API 客户端身份

NGINX-Part-of-F5-horiz-black-type-RGB
Liam Crilly 缩略图
Liam Crilly
Published December 02, 2021

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

NGINX Plus R10 及更高版本可直接验证 JWT。本文描述了如何将 NGINX Plus 用作 API 网关,为 API 端点提供前端,并使用 JWT 来验证客户端应用的身份。

仅 NGINX Plus 提供原生 JWT 支持(NGINX 开源版不提供)。

编者按 – 本文于 2021 年 12 月进行了更新,使用了 NGINX Plus R25 中引入的 auth_jwt_require 指令。有关详细的指令讨论,请参阅《NGINX Plus R25 详解》博文中的“自定义的 JWT 验证规则”。

NGINX Plus R15 及更高版本还可以控制 OpenID Connect 1.0 中的“授权码流”,支持集成大多数主要身份提供商。有关详细信息,请参阅《NGINX Plus R15 详解》<.htmla> 。

JWT 剖析

JWT 共包含三个部分:标头、有效负载和签名。它们的传输流程如下所示。为方便阅读,我们添加了换行符(实际的 JWT 是一个字符串)和颜色标记,以区分这三个部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0=.VGYHWPterIaLjRi0LywgN3jnDUQbSsFptUw99g2slfc

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

编码解码
标头eyJhbGciOiJIUzI1NiIsInR5cCI6Ik
pXVCJ9
{
    "alg": "HS256",
    "typ": "JWT"
}
有效负载ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0={
    "sub": "lc1",
    "email": "liam.crilly@nginx.com",
}

JWT 标准定义了几种签名算法。示例中的 HS256 这个值指的是 HMAC SHA 256,我们在本文中的所有 JWT 示例都使用该算法。NGINX Plus 支持标准中定义的 HSxxxRSxxxESxxx 签名算法。这种对 JWT 进行加密签名的能力使其成为身份验证凭证的理想之选。

将 JWT 用作 API 密钥

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

在将请求传递给 API 端点之前,API 网关通过查询密钥注册表来验证 API 密钥

为不同的 API 客户端应用不同的访问控制和策略是十分常见的。对于传统 API 密钥,需要进行查找,以将 API 密钥与一组属性匹配。对每个请求执行查找操作将对整体的系统延迟产生一定的影响。而 JWT 嵌入了这些属性,消除了单独查找的必要性。

将 JWT 用作 API 密钥是一种替代传统 API 密钥的高性能方案,它将最佳实践的认证技术与基于标准的模式相结合来进行身份属性交换。

NGINX Plus 在将请求传递到 API 端点前验证 JWT

配置 NGINX Plus 的 API 网关身份验证功能

NGINX Plus 的 API 网关身份验证功能配置很简单。

upstream api_server {    server 10.0.0.1;
    server 10.0.0.2;
}

server {
    listen 80;

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

我们所做的第一件事是在 upstream 块中指定托管 API 端点的服务器的地址。location 块指定了必须对以 /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‑encoded 编码值。我们通过运行以下命令获得此编码值:

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

kty 字段将密钥类型定义为对称密钥(字节序列)。最后,kid(密钥 ID)字段为该 JSON Web Key 定义了一个序列号(此处为 0001),这让我们可以在同一个文件(由 auth_jwt_key_file 指令命名)中支持多个密钥,并管理这些密钥以及具有其签名的 JWT 的生命周期。

现在,我们可以向 API 客户颁发 JWT 了。

向 API 客户端颁发 JWT

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

{
  "typ":"JWT",
  "alg":"HS256",
  "kid":"0001"
}

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

接下来,我们定义 JWT 有效负载:

{  "name":"Quotation System",
  "sub":"quotes",
  "iss":"My API Gateway"
}

sub (subject) 字段是 name 字段中完整值的唯一标识符。iss 字段描述了 JWT 的颁发者,如果您的 API 网关也接受来自第三方颁发者或中央身份管理系统的 JWT,那么这个字段将很有用处。

现在,我们拥有了创建 JWT 所需的一切,下面我们按照以下步骤来正确编码和签名。为方便阅读,我们将命令和编码值分散在多行显示,但实际上每一个都是单行输入或显示的。

  1. 分别对标头和有效负载进行 flatten 和 Base64URL 编码。

    $ echo -n '{"typ":"JWT","alg":"HS256","kid":"0001"}' | base64 | tr '+/' '-_' | tr -d '='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ
    
    $ echo -n '{"name":"Quotation System","sub":"quotes","iss":"My API Gateway"}' | 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 "Authorization: Bearer `cat quotes.jwt`" http://localhost/products/widget1

第 5 步中的 curl 命令将 JWT 以 Bearer Token 的形式发送给 NGINX Plus,这是 NGINX Plus 的默认选项。NGINX Plus 也可以从 cookie 或查询字符串参数中获得 JWT;要配置此功能,请在 auth_jwt 指令中添加 token= 参数。举例来说,通过下面的配置,NGINX Plus 可以验证这个 curl 命令发送的 JWT:

$ curl http://localhost/products/widget1?apijwt=`cat quotes.jwt`
server {
    listen 80;

    location /products/ {
        auth_jwt "Products API" token=$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_ 前缀添加到所需字段(例如 for the sub 声明的 $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;

server {
    listen 80;

    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;

        limit_req zone=10rps_per_client;

        proxy_pass http://api_server;
        proxy_set_header API-Client $jwt_claim_sub;

        access_log /var/log/nginx/access_jwt.log jwt;
    }
}

log_format 指令定义了一个名为 jwt 的新格式,它在常用日志格式的基础上增加了两个额外的字段:$jwt_header_algg 和$jwt_claim_sub。在 location 块中,我们使用 access_log 指令写入日志,日志包含从经过验证的 JWT 中获得的值。

在此示例中,我们还使用基于声明的变量,它可以按 API 客户端(而非按 IP 地址)来提供 API 速率限制。当一个门户嵌入了多个 API 客户端,并且无法通过 IP 地址进行区分时,此功能特别有用。 limit_req_zone 指令将 JWT sub 声明用作计算速率限制的密钥,然后通过添加 limit_req 指令将其应用到 location 块。

最后,当请求被代理到 API 端点时,我们将把 JWT 主题作为新的 HTTP 标头提供。proxy_set_header 指令增加了一个叫做 API‑Client 的 HTTP 标头,API 端点可轻松使用它。因此,API 端点不需要执行任何 JWT 处理逻辑。随着 API 端点数量的增加,此特性变得越来越重要。

注销 JWT

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

在本示例中,我们根据令牌(在$jwt_status 变量中捕获)中的 sub 声明的值,将 $jwt_status 变量设置为 01。然后,我们使用 location 块中的 auth_jwt_require 指令进一步验证(或拒绝)该令牌。为了保持有效性,$jwt_status 变量不能为空,也不能等于0 (zero)。

map $jwt_claim_sub $jwt_status {    "quotes" "revoked";
    "test"   "revoked";
    default  "";
}

server {
    listen 80;

    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;

        if ( $jwt_status = "revoked" ) {
            return 403;
        }

        proxy_pass http://api_server;
    }
}

结语

JSON Web Token 非常适合提供对 API 的身份验证访问。对于 API 客户端开发人员来说,它们就像传统 API 密钥一样易于处理,并且它们还向 API 网关提供身份信息(否则需要进行数据库查找)。NGINX Plus 可基于 JWT 本身所包含的信息,为 JWT 身份验证和复杂配置解决方案提供支持。如果再结合其他 API 网关功能,NGINX Plus 还支持您快速、可靠、可扩展且安全地交付基于 API 的服务。

如欲试用 NGINX Plus 的 JWT,请立即下载 30 天免费试用版,或联系我们讨论您的用例。


"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."