跳转到内容

JWT 认证

功能说明

jwt-auth 插件实现了基于 JWT(JSON Web Tokens) 进行认证鉴权的功能,支持从 HTTP 请求的 URL 参数、请求头、Cookie 字段解析 JWT,同时验证该 Token 是否有权限访问。

配置字段

全局配置

名称数据类型填写要求默认值描述
consumersarray of object必填-配置服务的调用者,用于对请求进行认证
global_authbool选填-若配置为 true,则全局生效认证机制; 若配置为 false,则只对做了配置的域名和路由生效认证机制; 若不配置则仅当没有域名和路由配置时全局生效(兼容机制)

consumers 中每一项的配置字段说明如下:

名称数据类型填写要求默认值描述
namestring必填-配置该 consumer 的名称
jwksstring必填-https://www.rfc-editor.org/rfc/rfc7517 指定的 json 格式字符串,是由验证 JWT 中签名的公钥(或对称密钥)组成的 Json Web Key Set
issuerstring必填-JWT 的签发者,需要和 payload 中的 iss 字段保持一致
claims_to_headersarray of object选填-抽取 JWT 的 payload 中指定字段,设置到指定的请求头中转发给后端
from_headersarray of object选填[{“name”:“Authorization”,“value_prefix”:“Bearer ”}]从指定的请求头中抽取 JWT
from_paramsarray of string选填access_token从指定的 URL 参数中抽取 JWT
from_cookiesarray of string选填-从指定的 cookie 中抽取 JWT
clock_skew_secondsnumber选填60校验 JWT 的 exp 和 iat 字段时允许的时钟偏移量,单位为秒
keep_tokenbool选填true转发给后端时是否保留 JWT

注意:

  • 只有当from_headers,from_params,from_cookies均未配置时,才会使用默认值

from_headers 中每一项的配置字段说明如下:

名称数据类型填写要求默认值描述
namestring必填-抽取 JWT 的请求 header
value_prefixstring必填-对请求 header 的 value 去除此前缀,剩余部分作为 JWT

claims_to_headers 中每一项的配置字段说明如下:

名称数据类型填写要求默认值描述
claimstring必填-JWT payload 中的指定字段,要求必须是字符串或无符号整数类型
headerstring必填-从payload 取出字段的值设置到这个请求头中,转发给后端
overridebool选填truetrue 时,存在同名请求头会进行覆盖;false 时,追加同名请求头

域名和路由级配置

名称数据类型填写要求默认值描述
allowarray of string必填-对于符合匹配条件的请求,配置允许访问的 consumer 名称

注意:

  • 对于通过认证鉴权的请求,请求的header会被添加一个X-Mse-Consumer字段,用以标识调用者的名称。

配置示例

对特定路由或域名开启

以下配置将对网关特定路由或域名开启 Jwt Auth 认证和鉴权,注意如果一个 JWT 能匹配多个 jwks,则按照配置顺序命中第一个匹配的 consumer

全局配置

consumers:
- name: consumer1
issuer: abcd
jwks: |
{
"keys": [
{
"kty": "oct",
"kid": "123",
"k": "hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew",
"alg": "HS256"
}
]
}
- name: consumer2
issuer: abc
jwks: |
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "123",
"alg": "RS256",
"n": "i0B67f1jggT9QJlZ_8QL9QQ56LfurrqDhpuu8BxtVcfxrYmaXaCtqTn7OfCuca7cGHdrJIjq99rz890NmYFZuvhaZ-LMt2iyiSb9LZJAeJmHf7ecguXS_-4x3hvbsrgUDi9tlg7xxbqGYcrco3anmalAFxsbswtu2PAXLtTnUo6aYwZsWA6ksq4FL3-anPNL5oZUgIp3HGyhhLTLdlQcC83jzxbguOim-0OEz-N4fniTYRivK7MlibHKrJfO3xa_6whBS07HW4Ydc37ZN3Rx9Ov3ZyV0idFblU519nUdqp_inXj1eEpynlxH60Ys_aTU2POGZh_25KXGdF_ZC_MSRw"
}
]
}
global_auth: false

路由级配置

对 route-a 和 route-b 这两个路由做如下配置:

allow:
- consumer1

对 *.exmaple.com 和 test.com 在这两个域名做如下配置:

allow:
- consumer2

每条匹配规则下的 allow 字段用于指定该匹配条件下允许访问的调用者列表;

若是在控制台进行配置,此例指定的 route-aroute-b 即在控制台创建路由时填写的路由名称,当匹配到这两个路由时,将允许 nameconsumer1 的调用者访问,其他调用者不允许访问;

此例指定的 *.example.comtest.com 用于匹配请求的域名,当发现域名匹配时,将允许 nameconsumer2 的调用者访问,其他调用者不允许访问。

认证成功后,请求的header中会被添加一个 X-Mse-Consumer 字段,其值为调用方的名称,例如 consumer-1

根据该配置,下列请求可以允许访问

假设以下请求会匹配到 route-a 这条路由。

将 JWT 设置在 URL 参数中。

Terminal window
curl 'http://xxx.hello.com/test?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4'

将 JWT 设置在 HTTP 请求头中。

Terminal window
curl http://xxx.hello.com/test -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4'

认证鉴权通过后,请求的 header 中会被添加一个 X-Mse-Consumer 字段,在此例中其值为 consumer1,用以标识调用方的名称。

下列请求将拒绝访问

请求未提供 JWT,返回 401。

Terminal window
curl http://xxx.hello.com/test

根据请求提供的 JWT 匹配到的调用者无访问权限,返回 403。

Terminal window
## consumer1 不在 *.example.com 的 allow 列表里
curl 'http://xxx.example.com/test' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4'

常见错误码说明

HTTP 状态码出错信息原因说明
401Jwt missing请求头未提供JWT
401Jwt expiredJWT已经过期
401Jwt verification failsJWT payload校验失败,如iss不匹配
403Access Denied无权限访问当前路由

机制说明

1 基于 token 的认证

1.1 简介

很多对外开放的 API 需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token 就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的 Web 应用授权,为应用的扩展提供了便利。

1.2 流程描述

上图是网关利用 JWT 实现认证的整个业务流程时序图,下面我们用文字来详细描述图中标注的步骤:

  1. 客户端向 API 网关发起认证请求,请求中一般会携带终端用户的用户名和密码;

  2. 网关将请求直接转发给后端服务;

  3. 后端服务读取请求中的验证信息(比如用户名、密码)进行验证,验证通过后使用私钥生成标准的 token,返回给网关;

  4. 网关将携带 token 的应答返回给客户端,客户端需要将这个 token 缓存到本地;

  5. 客户端向 API 网关发送业务请求,请求中携带 token;

  6. 网关使用用户设定的公钥对请求中的 token 进行验证,验证通过后,将请求透传给后端服务;

  7. 后端服务进行业务处理后应答;

  8. 网关将业务应答返回给客户端。

在这个整个过程中, 网关利用 token 认证机制,实现了用户使用自己的用户体系对自己API进行授权的能力。下面我们就要介绍网关实现 token 认证所使用的结构化令牌 Json Web Token(JWT)。

1.3 JWT

1.3.1 简介

Json Web Toke(JWT),是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准 RFC7519。JWT 一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。

1.3.2 JWT的构成

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

如上面的例子所示,JWT 就是一个字符串,由三部分构成:

  • Header(头部)
  • Payload(数据)
  • Signature(签名)

Header

JWT 的头部承载两个信息:

  • 声明类型,这里是 JWT
  • 声明加密的算法

网关支持的加密算法如下:

ES256, ES384, ES512,
HS256, HS384, HS512,
RS256, RS384, RS512,
PS256, PS384, PS512,
EdDSA

完整的头部就像下面这样的 JSON:

{
'typ': 'JWT',
'alg': 'HS256'
}

然后将头部进行 Base64 编码(该编码是可以对称解码的),构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload

载荷就是存放有效信息的地方。定义细节如下:

iss:令牌颁发者。表示该令牌由谁创建,该声明是一个字符串
sub: Subject Identifier,iss 提供的终端用户的标识,在 iss 范围内唯一,最长为 255 个 ASCII 个字符,区分大小写
aud:Audience(s),令牌的受众,分大小写的字符串数组
exp:Expiration time,令牌的过期时间戳。超过此时间的 token 会作废, 该声明是一个整数,是 1970 年 1 月 1 日以来的秒数
iat: 令牌的颁发时间,该声明是一个整数,是 1970 年 1 月 1 日以来的秒数
jti: 令牌的唯一标识,该声明的值在令牌颁发者创建的每一个令牌中都是唯一的,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击。

也可以新增用户系统需要使用的自定义字段,比如下面的例子添加了 name 用户昵称:

{
"sub": "1234567890",
"name": "John Doe"
}

然后将其进行 Base64 编码,得到JWT的第二部分:

JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE

Signature

这个部分需要 Base64 编码后的 Header 和 Base64 编码后的 Payload 使用 . 连接组成的字符串,然后通过 Header 中声明的加密方式进行加密($secret 表示用户的私钥),然后就构成了 jwt 的第三部分。

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');

将这三部分用 . 连接成一个完整的字符串,就构成了 1.3.2 节最开始的 JWT 示例。

1.3.3 时效

网关会验证 token 中的 exp 字段,一旦这个字段过期了,网关会认为这个 token 无效而将请求直接打回。过期时间这个值必须设置。

1.3.4 JWT 的几个特点
  1. JWT 默认是不加密,不能将秘密数据写入 JWT。
  2. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  3. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  4. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  5. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。

2 用户系统如何应用 JWT 插件保护 API

2.1 生成一对 JWK(JSON Web 密钥)

方法一、在线生成:

用户可以在这个站点https://mkjwk.org 生成用于 token 生成与验证的私钥与公钥, 私钥用于授权服务签发 JWT,公钥配置到 JWT 插件中用于网关对请求验签,注意网关使用的 jwks 格式配置,下图中Public Key需要放到keys结构体中,如:{"keys":[{"kty":"RSA","e":"AQAB",...}]}

img

方法二、本地生成:

本文应用 Java 示例说明,其他语言用户也可以找到相关的工具生成密钥对。 新建一个 Maven 项目,加入如下依赖:

<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.0</version>
</dependency>

使用如下的代码生成一对RSA密钥:

RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
final String publicKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
final String privateKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);

2.2 使用 JWK 中的私钥实现颁发 token 的认证服务

需要使用 2.1 节中在线生成的 Keypair JSON 字符串(三个方框内的第一个)或者本地生成的 privateKeyString JSON 字符串作为私钥来颁发 token,用于授权可信的用户访问受保护的 API,具体实现可以参考下方示例。 向客户颁发token的形式由用户根据具体的业务场景决定,可以将颁发token的功能部署到生产环境,配置成普通 API 后由访问者通过用户名密码获得,也可以直接在本地环境生成 token 后,直接拷贝给指定用户使用。

import java.security.PrivateKey;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.lang.JoseException;
public class GenerateJwtDemo {
public static void main(String[] args) throws JoseException {
String keyId = "uniq_key";
//使用本文 2.1 节生成的 Keypair
String privateKeyJson = "{\n"
+ " \"kty\": \"RSA\",\n"
+ " \"d\": "
+
"\"O9MJSOgcjjiVMNJ4jmBAh0mRHF_TlaVva70Imghtlgwxl8BLfcf1S8ueN1PD7xV6Cnq8YenSKsfiNOhC6yZ_fjW1syn5raWfj68eR7cjHWjLOvKjwVY33GBPNOvspNhVAFzeqfWneRTBbga53Agb6jjN0SUcZdJgnelzz5JNdOGaLzhacjH6YPJKpbuzCQYPkWtoZHDqWTzCSb4mJ3n0NRTsWy7Pm8LwG_Fd3pACl7JIY38IanPQDLoighFfo-Lriv5z3IdlhwbPnx0tk9sBwQBTRdZ8JkqqYkxUiB06phwr7mAnKEpQJ6HvhZBQ1cCnYZ_nIlrX9-I7qomrlE1UoQ\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"n\": \"vCuB8MgwPZfziMSytEbBoOEwxsG7XI3MaVMoocziP4SjzU4IuWuE_DodbOHQwb_thUru57_Efe"
+
"--sfATHEa0Odv5ny3QbByqsvjyeHk6ZE4mSAV9BsHYa6GWAgEZtnDceeeDc0y76utXK2XHhC1Pysi2KG8KAzqDa099Yh7s31AyoueoMnrYTmWfEyDsQL_OAIiwgXakkS5U8QyXmWicCwXntDzkIMh8MjfPskesyli0XQD1AmCXVV3h2Opm1Amx0ggSOOiINUR5YRD6mKo49_cN-nrJWjtwSouqDdxHYP-4c7epuTcdS6kQHiQERBd1ejdpAxV4c0t0FHF7MOy9kw\"\n"
+ "}";
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
//过期时间一定要设置
NumericDate date = NumericDate.now();
date.addSeconds(120*60);
claims.setExpirationTime(date);
claims.setNotBeforeMinutesInThePast(1);
claims.setSubject("YOUR_SUBJECT");
claims.setAudience("YOUR_AUDIENCE");
//添加自定义参数,所有值请都使用String类型
claims.setClaim("userId", "1213234");
claims.setClaim("email", "userEmail@youapp.com");
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
jws.setKeyIdHeaderValue(keyId);
jws.setPayload(claims.toJson());
PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyJson)).getPrivateKey();
jws.setKey(privateKey);
String jwtResult = jws.getCompactSerialization();
System.out.println("Generate Json Web token , result is " + jwtResult);
}
}