阿里云API网关在JSON Web Token(JWT)这种结构化令牌的基础上实现了一套基于用户体系对用户的API进行授权访问的机制,满足用户个性化安全设置的需求。
一、基于token的认证
1.1 简介
很多对外开放的API需要识别请求者的身份,并据此判断所请求的资源是否可以返回给请求者。token就是一种用于身份验证的机制,基于这种机制,应用不需要在服务端保留用户的认证信息或者会话信息,可实现无状态、分布式的Web应用授权,为应用的扩展提供了便利。
1.2 流程描述
上图是API网关利用JWT鉴权插件实现认证的整个业务流程时序图,下面我们用文字来详细描述图中标注的步骤:
客户端向API网关发送请求,请求中携带token;
API网关使用用户插件中配置的公钥对请求中的token进行验证,验证通过后,将请求透传给后端服务;
后端服务进行处理后返回应答;
API网关将后端服务应答返回给客户端。
在整个过程中,API网关利用token认证机制,实现了用户使用自己的用户体系对API进行授权的能力。下面我们就要介绍API网关实现token认证所使用的结构化令牌Json Web Token(JWT)。
1.3 JWT
1.3.1 简介
Json Web Token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准RFC7519。JWT一般可以用作独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,特别适用于分布式站点的登录场景。
1.3.2 JWT的构成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如上面的例子所示,JWT就是一个字符串,由三部分构成:
Header(头部)
Payload(数据)
Signature(签名)
Header
JWT的头部承载两个信息:
声明类型,这里是JWT
声明加密的算法
完整的头部就像下面这样的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的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');
将这三部分用 .
连接成一个完整的字符串,就构成了 1.3.2 节最开始的JWT示例。
1.3.3 授权范围与时效
API网关会认为用户颁发的token有权利访问整个分组下的所有绑定JWT插件的API。如果需要更细粒度的权限管理,还需要后端服务自行解开token进行权限认证。API网关会验证token中的exp字段,一旦这个字段过期了,API网关会认为这个token无效而将请求直接打回。过期时间这个值必须设置,并且过期时间一定要小于7天。
1.3.4 JWT的几个特点
JWT 默认是不加密,不能将秘密数据写入 JWT。
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用HTTPS 协议传输。
二、用户系统如何应用JWT插件保护API
2.1 生成一对JWK(JSON Web 密钥)
方法一、在线生成:
用户可以在这个站点https://mkjwk.org 生成用于token生成与验证的私钥与公钥, 私钥用于授权服务签发JWT,公钥配置到JWT插件中用于API网关对请求验签,目前API网关支持的密钥对的加密算法为RSA SHA256,密钥对的加密的位数为2048。
方法二、本地生成:
本文应用Java示例说明,其他语言用户也可以找到相关的工具生成密钥对。 新建一个Maven项目,加入如下依赖:
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.0</version>
</dependency>
使用如下的代码生成一对RSA密钥:
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("authServer");
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 后,直接拷贝给指定用户使用。
2.3 将JWK中的公钥配置到JWT插件中
---
parameter: X-Token # 从指定的参数中获取JWT, 对应API的参数
parameterLocation: header # API为映射模式时可选, API为透传模式下必填, 用于指定JWT的读取位置, 仅支持`query`,`header`
claimParameters: # claims参数转换, 网关会将jwt claims映射为后端参数
- claimName: aud # claim名称,支持公共和私有
parameterName: X-Aud # 映射后参数名称
location: header # 映射后参数位置, 支持`query,header,path,formData`
- claimName: userId # claim名称,支持公共和私有
parameterName: userId # 映射后参数名称
location: query # 映射后的参数位置, 支持`query,header,path,formData`
preventJtiReplay: false # 是否开启针对`jti`的防重放检查, 默认false
#
# `Json Web Key`的`Public Key`, 即本文2.1节生成的公钥部分
jwk:
kty: RSA
e: AQAB
use: sig
kid: uniq_key
alg: RS256
n: qSVxcknOm0uCq5vGsOmaorPDzHUubBmZZ4UXj-9do7w9X1uKFXAnqfto4TepSNuYU2bA_-tzSLAGBsR-BqvT6w9SjxakeiyQpVmexxnDw5WZwpWenUAcYrfSPEoNU-0hAQwFYgqZwJQMN8ptxkd0170PFauwACOx4Hfr-9FPGy8NCoIO4MfLXzJ3mJ7xqgIZp3NIOGXz-GIAbCf13ii7kSStpYqN3L_zzpvXUAos1FJ9IPXRV84tIZpFVh2lmRh0h8ImK-vI42dwlD_hOIzayL1Xno2R0T-d5AwTSdnep7g-Fwu8-sj4cCRWq3bd61Zs2QOJ8iustH0vSRMYdP5oYQ
2.4 JWT插件绑定API
在插件列表页找到创建好的JWT鉴权插件,单击按钮,在弹出框中添加指定分组和环境下的API到弹出框右侧API列表中,单击,绑定完成。
目前,控制台的API调试功能并没有支持JWT插件,建议用户通过Postman或者直接在系统命令行中应用curl
命令测试绑定JWT插件的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 {
//使用在API网关设置的keyId
String keyId = "uniq_key";
//使用本文2.1节生成的Keypare
String privateKeyJson = "{\n"
+ " \"kty\": \"RSA\",\n"
+ " \"d\": "
+
"\"O9MJSOgcjjiVMNJ4jmBAh0mRHF_TlaVva70Imghtlgwxl8BLfcf1S8ueN1PD7xV6Cnq8YenSKsfiNOhC6yZ_fjW1syn5raWfj68eR7cjHWjLOvKjwVY33GBPNOvspNhVAFzeqfWneRTBbga53Agb6jjN0SUcZdJgnelzz5JNdOGaLzhacjH6YPJKpbuzCQYPkWtoZHDqWTzCSb4mJ3n0NRTsWy7Pm8LwG_Fd3pACl7JIY38IanPQDLoighFfo-Lriv5z3IdlhwbPnx0tk9sBwQBTRdZ8JkqqYkxUiB06phwr7mAnKEpQJ6HvhZBQ1cCnYZ_nIlrX9-I7qomrlE1UoQ\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"kid\": \"myJwtKey\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"n\": \"vCuB8MgwPZfziMSytEbBoOEwxsG7XI3MaVMoocziP4SjzU4IuWuE_DodbOHQwb_thUru57_Efe"
+
"--sfATHEa0Odv5ny3QbByqsvjyeHk6ZE4mSAV9BsHYa6GWAgEZtnDceeeDc0y76utXK2XHhC1Pysi2KG8KAzqDa099Yh7s31AyoueoMnrYTmWfEyDsQL_OAIiwgXakkS5U8QyXmWicCwXntDzkIMh8MjfPskesyli0XQD1AmCXVV3h2Opm1Amx0ggSOOiINUR5YRD6mKo49_cN-nrJWjtwSouqDdxHYP-4c7epuTcdS6kQHiQERBd1ejdpAxV4c0t0FHF7MOy9kw\"\n"
+ "}";
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
//过期时间一定要设置,并且小于7天
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", "userEm***@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);
}
}
上述示例中有以下几个地方需要重点关注:
keyId需要三个环节都一致,且全局唯一:
privateKeyJson 使用2.1节中在线生成的
Keypair
JSON字符串(三个方框内的第一个)或者本地生成的privateKeyString
JSON字符串。过期时间一定要设置,并且小于7天。
添加自定义参数,请都使用String类型的值。
四、API网关错误应答列表
Status | Code | Message | Description |
400 | I400JR | JWT required | 未找到JWT参数 |
403 | S403JI | Claim jti is required when preventJtiReplay:true | 当在JWT授权插件中配置了防重放功能时,请求未提供有效的jti |
403 | S403JU | Claim jti in JWT is used | 当在JWT授权插件中配置了防重放功能时,请求提供的jti已被使用 |
403 | A403JT | Invalid JWT: ${Reason} | 请求中提供的JWT非法 |
400 | I400JD | JWT Deserialize Failed: ${Token} | 请求中提供的JWT解析失 |
403 | A403JK | No matching JWK, kid:${kid} not found | 请求JWT中的kid没有匹配的JWK |
403 | A403JE | JWT is expired at ${Date} | 请求中提供的JWT已过期 |
400 | I400JP | Invalid JWT plugin config: ${JWT} | JWT授权插件配置错误 |
当出现非预期应答码时,请检查HTTP应答中的X-Ca-Error-Code
头中获取ErrorCode
,从X-Ca-Error-Message
头中获取ErrorMessage
当出现A403JT
或I400JD
错误码时,可访问jwt.io
网站来检查自己的Token
合法性与格式。