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

NGINX | 2021年12月02日

JSON Web令牌(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 pXVCJ9 {
    "alg": "HS256",
    "typ": "JWT"
}
有效负载 ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0= {
    "sub": "lc1",
    "email": "liam.crilly@nginx.com",
}

The JWT标准定义了多种签名算法。示例中的值HS256指HMAC SHA‑256,本博文中的所有JWT示例均使用该算法。NGINX Plus支持HSxxx、RSxxx和ESxxx 签名算法,这些算法均在标准中定义。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配置非常简单。

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状态码一并返回。

The 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字段中定义,此处为明文字符串fantasticjwt经Base64URL 编码后的值。我们通过运行以下命令获得该编码值:

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

The 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"
}

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

接下来,定义JWT载荷:

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

The sub(主题)字段是name字段完整值的唯一标识符。iss字段用于说明JWT的颁发者。如果API网关还接受第三方颁发者或集中式身份管理系统颁发的JWT,此字段会很有用。

现在,创建JWT所需的一切都已准备就绪。接下来,按照以下步骤对其进行正确编码和签名。命令和编码值分多行显示只是为了便于阅读;实际输入或显示时,每项都位于同一行。

  1. 分别将标头和有效负载转换为单行,并进行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
  1. 使用句点(.)连接编码后的标头和有效负载,并将结果赋值给HEADER_PAYLOAD变量。
$ HEADER_PAYLOAD=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsIm lzcyI6Ik15IEFQSSBHYXRld2F5In0
  1. 使用我们的对称密钥对标头和有效负载进行签名,并对签名进行Base64URL编码。
$ echo -n $HEADER_PAYLOAD | openssl dgst -binary -sha256 -hmac fantasticjwt | base64 | tr '+/' '-_' | tr -d '='ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
  1. 将编码后的签名附加到标头和有效负载之后。
$ echo $HEADER_PAYLOAD.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I > quotes.jwt
  1. 向API网关发出经过身份验证的请求以进行测试(本例中,网关在localhost上运行)。
$ curl -H "Authorization: Bearer `cat quotes.jwt`" http://localhost/products/widget1

步骤 5 中的curl命令以Bearer Token的形式将JWT发送到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的签发者、获发JWT的用户以及预期接收方。验证JWT后,NGINX Plus可以将标头和有效载荷中的所有字段作为变量访问。要访问所需字段,请在字段名称前添加前缀$jwt_header_或$jwt_claim_(例如,使用$jwt_claim_sub访问sub声明)。因此,无需在API本身中实现JWT处理,即可轻松将JWT中包含的信息代理到API端点。

此配置示例展示了部分高级功能。

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;
    }
}

The log_format指令定义了一种名为jwt的新格式,在通用日志格式的基础上增加了两个字段:$jwt_header_alg和$jwt_claim_sub。在location块中,使用access_log指令将从已验证JWT中获取的值写入日志。

在此示例中,我们还使用基于声明的变量,按API客户端而非IP地址实施API速率限制。当多个API客户端嵌入同一门户且无法通过IP地址加以区分时,这种方式尤其有用。limit_req_zone指令使用JWT的sub声明作为计算速率限制的键,然后通过添加location块中的limit_req指令来应用这些速率限制。

最后,当请求被代理到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变量的值设置为0或1。该值取决于令牌中sub声明的值(由$jwt_claim_sub变量捕获)。然后,我们在auth_jwt_require指令所在的location块中进一步验证(或拒绝)令牌。要使令牌有效,$jwt_status变量不得为空,也不得等于0(零)。

map $jwt_claim_sub $jwt_status { "quotes" 0; "test" 0; default 1;
}

server {
    listen 80;

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

        proxy_pass http://api_server;
    }
}

总结

JSON Web Token非常适合用于提供经过身份验证的API访问。对于API客户端开发人员而言,其处理方式与传统API密钥一样简单,同时还能向API网关提供身份信息,而这些信息通常需要查询数据库才能获取。NGINX Plus支持JWT身份验证,并可根据JWT本身包含的信息提供高级配置方案。结合其他API网关功能,NGINX Plus可帮助您快速、可靠、可扩展且安全地交付基于API的服务。

如需亲自试用NGINX Plus的JWT功能,立即开始30天免费试用,或联系我们,讨论您的用例。


分享
标签: 2021, F5 NGINX, Tech

关于作者

相关博客文章

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内容缓存方式的技巧。