全部产品
Search
文档中心

API 网关:基于JWT的token认证

更新时间:Mar 20, 2024

阿里云API网关在Json Web Token(JWT)这种结构化令牌的基础上实现了一套基于用户体系对用户的API进行授权访问的机制,满足用户个性化安全设置的需求。

一、基于token的认证

1.1 简介

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

1.2 流程描述

image

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

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

  2. API网关使用用户插件中配置的公钥对请求中的token进行验证,验证通过后,将请求透传给后端服务;

  3. 后端服务进行处理后返回应答;

  4. 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就是一个字符串,由三部分构成:

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的几个特点

  1. JWT 默认是不加密,不能将秘密数据写入 JWT。

  2. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

  3. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  4. 为了减少盗用,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插件中

  1. 登录 API网关控制台。

  2. 在左侧导航栏单击开放API——插件管理。

  3. 在插件管理页面,单击右上角的创建插件

  4. 在创建插件页面,插件类型选择JWT鉴权,下面是一个JWT鉴权插件的配置及说明,具体配置说明可以参考文档 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
  alg: RS256
  n: qSVxcknOm0uCq5vGsOmaorPDzHUubBmZZ4UXj-9do7w9X1uKFXAnqfto4TepSNuYU2bA_-tzSLAGBsR-BqvT6w9SjxakeiyQpVmexxnDw5WZwpWenUAcYrfSPEoNU-0hAQwFYgqZwJQMN8ptxkd0170PFauwACOx4Hfr-9FPGy8NCoIO4MfLXzJ3mJ7xqgIZp3NIOGXz-GIAbCf13ii7kSStpYqN3L_zzpvXUAos1FJ9IPXRV84tIZpFVh2lmRh0h8ImK-vI42dwlD_hOIzayL1Xno2R0T-d5AwTSdnep7g-Fwu8-sj4cCRWq3bd61Zs2QOJ8iustH0vSRMYdP5oYQ

2.4 JWT插件绑定API

在插件列表页找到刚刚创建好的JWT鉴权插件,单击绑定API按钮,在弹出框中添加指定分组和环境下的API到弹出框右侧API列表中,单击确定,绑定完成。

目前,控制台的API调试功能并没有支持JWT插件,建议用户通过Postman或者直接在系统命令行中应用curl命令测试绑定JWT插件的API。

三、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合法性与格式。

四、颁发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";
          //使用本文3.2节生成的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);
    }
}

上述示例中有以下几个地方需要重点关注:

  1. keyId需要三个环节都一致,且全局唯一:

  1. privateKeyJson 使用2.1节中在线生成的 Keypair JSON字符串(三个方框内的第一个)或者本地生成的

    privateKeyString JSON字符串;

  2. 过期时间一定要设置,并且小于7天;

  3. 添加自定义参数,所有值请都使用String类型。

两个鬼故事消毒产品商标起名李和杜怎么起名字养殖企业起公司名称意大利冰激凌加盟无能力者的奈奈写给姓于的男孩起名字羊汤馆起个名字美名网公司起名孩子起名智能起名网怎么给小孩起名字大全起姓名大全免费名字大全剩余的公主温度检测软件起名免费起名曾氏游戏起名助手帝国时代2高清版公司起名的注意事项鬼接男猪宝宝起名用字公司起名字免费测名马氏名字姑娘起名宝宝取名免费起名大全起人名建安公司起名北京轻舟装饰100分陈姓男孩起名起名字男孩带钰字的孙权劝学原文翻译葛雄天行战记少年生前被连续抽血16次?多部门介入两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”淀粉肠小王子日销售额涨超10倍高中生被打伤下体休学 邯郸通报单亲妈妈陷入热恋 14岁儿子报警何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言张家界的山上“长”满了韩国人?男孩8年未见母亲被告知被遗忘中国拥有亿元资产的家庭达13.3万户19岁小伙救下5人后溺亡 多方发声315晚会后胖东来又人满为患了张立群任西安交通大学校长“重生之我在北大当嫡校长”男子被猫抓伤后确诊“猫抓病”测试车高速逃费 小米:已补缴周杰伦一审败诉网易网友洛杉矶偶遇贾玲今日春分倪萍分享减重40斤方法七年后宇文玥被薅头发捞上岸许家印被限制高消费萧美琴窜访捷克 外交部回应联合利华开始重组专访95后高颜值猪保姆胖东来员工每周单休无小长假男子被流浪猫绊倒 投喂者赔24万小米汽车超级工厂正式揭幕黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发当地回应沈阳致3死车祸车主疑毒驾恒大被罚41.75亿到底怎么缴妈妈回应孩子在校撞护栏坠楼外国人感慨凌晨的中国很安全杨倩无缘巴黎奥运校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变王树国卸任西安交大校长 师生送别手机成瘾是影响睡眠质量重要因素国产伟哥去年销售近13亿阿根廷将发行1万与2万面值的纸币兔狲“狲大娘”因病死亡遭遇山火的松茸之乡“开封王婆”爆火:促成四五十对奥巴马现身唐宁街 黑色着装引猜测考生莫言也上北大硕士复试名单了德国打算提及普京时仅用姓名天水麻辣烫把捣辣椒大爷累坏了

两个鬼故事 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化