云原生API网关支持全局认证、路由配置认证和消费者鉴权,以实现对API访问的控制、安全性和策略管理,确保只有授权的请求才能访问服务。本文介绍如何配置消费者鉴权,对API的调用者进行身份验证,确保请求是由授权的API消费者发起。
背景信息
相比全局认证鉴权适用于统一登录认证等ToC场景,路由、API开启消费者认证,适用于授权API给合作伙伴等ToB场景。
对比项 | 全局认证鉴权 | 路由认证+消费者鉴权 |
适用场景 | 统一登录认证等ToC场景。 | 授权API给合作伙伴等ToB场景。 |
核心差异 | 开启认证的同时也开启鉴权。 | 开启认证后,需要额外做鉴权配置。 |
配置入口 | 。 |
|
认证方式配置(以JWT认证为例) |
|
|
鉴权方式配置 | 创建配置时填写黑名单或白名单的域名和路径(Path)列表。
|
|
注意事项
当用户在消费者认证中开启认证后,认证策略会即时生效,如果此时路由或者API已发布,但是又未给路由或者API配置消费者和授权规则,则默认会拒绝所有访问请求。
配置消费者认证鉴权
对于未发布的API或路由,建议您在授权完成后,再发布路由或者API,此时认证鉴权策略会自动生效。
对于已发布的API或路由,建议您先在策略配置中将消费者认证置为关闭状态,等授权完成后,再开启策略,使其生效。
步骤一:开启路由或者API认证
登录云原生API网关控制台。在左侧导航栏,选择API,并在顶部菜单栏选择地域。在API列表页面,单击目标API名称:
HTTP API路由认证
进入路由列表页面,单击目标路由,选择策略配置页签。
单击消费者认证,配置认证方式和开启状态。
参数 | 描述 |
开启 | 开启后,认证鉴权生效。 |
认证方式 | 当前路由认证消费者时使用的认证方式。目前支持JWT、AK/SK、API key的认证方式。 |
REST API认证
进入详情页后,单击全部接口(API级策略&插件配置),在右侧的策略插件树中进行配置,在入站处理单击启用策略/插件。
在启用策略/插件控制面板,选择添加策略页签,单击消费者认证卡片。
配置认证方式和开启状态。
参数
描述
开启
开启后,认证鉴权生效。
重要对于已发布的API或路由,建议您先挂载消费者认证,待授权完成后,再启用该策略。
认证方式
当前路由认证消费者时使用的认证方式。目前支持JWT、AK/SK、API key的认证方式。
WebSocket API暂不支持路由认证。
步骤二:创建消费者
登录云原生API网关控制台。
在左侧导航栏,选择消费者。
在消费者页面左上角,单击创建消费者。
在创建消费者页面,填写消费者名称,选择如下认证方式并单击创建。
JWT认证
配置参数说明如下:
配置项
描述
消费者名称
自定义消费者的名称。
描述
对消费者进行描述。
认证方式
当前消费者支持的认证方式,目前主要支持JWT、AK/SK、API Key三种认证方式。
密钥类型
对称密钥:生成一份默认的JWKS配置(每个消费者不同),包含加密或者解密Token时使用的密钥。
非对称密钥:需要您自己填写完整的JWKS配置,使用私钥加密Token。网关根据JWKS中配置的公钥进行解密。
JWKS
设置JWKS配置,JWKS规范说明请参考JSON Web Key (JWK)。
JWT Token配置
设置JWT Token配置信息。
类型:Token参数类型,默认Header。
Key:Token参数名称。
前缀:Token参数名的前缀。设置需要校验的Token参数信息,默认是以Bearer为前缀放在Authorization Header中,例如Authorization: Bearer token。
是否透传:选中Token参数透传,表示透传此Token参数到后端服务。
JWT Payload 内消费者标识
指定从JWT Payload中的Key以及对应Value来识别为当前消费者。默认提供一对标识,Key为uid,Value为随机字符串,可以自行修改。
JWT Token中的Payload配置示例如下所示:
{ "uid": "11215ac069234abcb8944232b79ae711" }
AK/SK(HMAC)认证
配置参数说明如下:
配置项
描述
消费者名称
自定义消费者的名称。
消费者说明
对消费者进行描述。
认证方式
当前消费者支持的认证方式,目前主要支持JWT、AK/SK、API Key三种认证方式。
生成方式
系统生成:系统为您自动生成AK、SK配置。
自定义:自定义AK、SK配置。
API Key认证
配置参数说明如下:
配置项
描述
消费者名称
自定义消费者的名称。
消费者说明
对消费者进行描述。
认证方式
当前消费者支持的认证方式,目前主要支持JWT、AK/SK、API Key三种认证方式。
生成方式
系统生成:系统为您自动生成API Key凭证。
自定义:自定义API Key凭证及凭证来源。
凭证来源
凭证来源用于网关从请求的提取参数的位置和名称,支持如下:
Authorization: Bearer <token>
自定义 HTTP Header
自定义Query String
步骤三:授权给消费者
在左侧导航栏,单击消费者,进入消费者列表页面。
单击目标消费者进入详情页,选择消费者授权页签,单击授权。
在授权面板中,配置相关参数,单击确定。
添加API授权
参数
描述
API类型
选择REST API。
生效环境名称/ID
要授权的环境。
授权范围
支持对API或者接口进行访问控制。
选择API(授权范围为API)
授权的API。
生效API(授权范围为接口)
要授权接口所属的API。
选择接口(授权范围为接口)
要授权的接口。
添加路由授权
参数
描述
API类型
选择HTTP API。
授权范围
对路由进行授权。
生效API
要授权接口所属的API。
选择路由
要授权的路由。
步骤四:认证策略生效
使用消费者认证鉴权
JWT认证
JWT认证流程概览:
客户端向API网关发起认证请求,请求中一般会携带终端用户的用户名和密码。
网关将请求直接转发给后端服务。
后端服务读取请求中的验证信息(比如用户名、密码)进行验证,验证通过后使用私钥生成标准的Token,返回给网关。
网关将携带Token的应答返回给客户端,客户端需要将这个Token缓存到本地。
客户端向API网关发送业务请求,请求中携带Token。
网关使用用户设定的公钥对请求中的Token进行验证,验证通过后,将请求透传给后端服务。
后端服务进行业务处理后应答。
网关将业务应答返回给客户端。
下文主要介绍生成Token、客户端向网关发送请求、网关使用设定的公钥进行Token验证的过程。
认证服务生成Token
下文将通过Java示例来说明Token的生成方式,其他语言您也可使用相关的工具生成密钥对。
新建Maven项目并注入依赖。
首先,新建一个Maven项目,注入如下依赖项:
<dependency> <groupId>org.bitbucket.b_c</groupId> <artifactId>jose4j</artifactId> <version>0.7.0</version> </dependency>
选择Token生成方式。
您可以选择使用默认对称密钥示例生成Token和非对称密钥示例生成Token两种方式来生成Token。根据您的需求进行选择:
使用默认对称密钥示例生成Token
代码示例:
package org.example; import java.io.UnsupportedEncodingException; import java.security.PrivateKey; import org.jose4j.base64url.Base64; import org.jose4j.json.JsonUtil; import org.jose4j.jwk.OctJwkGenerator; import org.jose4j.jwk.OctetSequenceJsonWebKey; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; import org.jose4j.keys.HmacKey; import org.jose4j.lang.JoseException; import sun.lwawt.macosx.CSystemTray; public class Main { public static void main(String[] args) throws JoseException, UnsupportedEncodingException { //使用本文上述示例 String privateKeyJson = "{\n" + " \"k\": \"VoBG-oyqVoyCr9G56ozmq8n_rlDDyYMQOd_DO4GOkEY\",\n" + " \"kty\": \"oct\",\n" + " \"alg\": \"HS256\",\n" + "}"; JwtClaims claims = new JwtClaims(); claims.setGeneratedJwtId(); claims.setIssuedAtToNow(); //设置过期时间,并且小于7天 NumericDate date = NumericDate.now(); date.addSeconds(120*60); claims.setExpirationTime(date); claims.setNotBeforeMinutesInThePast(1); //添加自定义参数,所有值请都使用String类型 //设置消费者标识 claims.setClaim("uid", "11215ac069234abcb8944232b79ae711"); JsonWebSignature jws = new JsonWebSignature(); //设置加密算法 jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256); jws.setKey(new HmacKey(Base64.decode(JsonUtil.parseJson(privateKeyJson).get("k").toString()))); jws.setPayload(claims.toJson()); String jwtResult = jws.getCompactSerialization(); System.out.println("Generate Json Web token , result is \n " + jwtResult); } }
代码相关设置说明:
privateKeyJson
:即在创建消费者时使用的JWKS,可以在创建消费者时记录下自己使用的JWKS,也可以在创建消费者后,在消费者基础配置页获取JWKS。设置消费者标识。即
claims.setClaim("uid", "11215ac069234abcb8944232b79ae711")
,该消费者标识为创建消费者时控制台默认生成,也可以根据自身逻辑进行修改。您也可以在创建消费者后,在消费者基础配置页获取消费者标识。设置加密算法。即
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256)
,该加密算法要和JWKS保持一致。说明目前支持的加密算法有ES256、ES384、ES512、RS256、RS384、RS512、PS256、PS384、PS512、HS256、HS384、HS512和EdDSA。
使用对称加密的时,需要对"k"进行解码。
jws.setKey(new HmacKey(Base64.decode(JsonUtil.parseJson(privateKeyJson).get("k").toString())));
设置过期时间。过期时间需要小于7天,超出过期时间后,请重新生成Token,以保证Token的安全性。
... NumericDate date = NumericDate.now(); date.addSeconds(120*60); claims.setExpirationTime(date); claims.setNotBeforeMinutesInThePast(1); ...
根据自身业务需要,可以在JWKS的PAYLOAD中添加自定义参数。
使用非对称密钥示例生成Token
代码示例:
package org.example; import java.io.UnsupportedEncodingException; import java.security.PrivateKey; import org.jose4j.base64url.Base64; import org.jose4j.json.JsonUtil; import org.jose4j.jwk.OctJwkGenerator; import org.jose4j.jwk.OctetSequenceJsonWebKey; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.NumericDate; import org.jose4j.keys.HmacKey; import org.jose4j.lang.JoseException; import sun.lwawt.macosx.CSystemTray; public class Main { public static void main(String[] args) throws JoseException, UnsupportedEncodingException { //使用本文上述示例 String privateKeyJson = "{\n" + " \"k\": \"VoBG-oyqVoyCr9G56ozmq8n_rlDDyYMQOd_DO4GOkEY\",\n" + " \"kty\": \"oct\",\n" + " \"alg\": \"HS256\",\n" + "}"; JwtClaims claims = new JwtClaims(); claims.setGeneratedJwtId(); claims.setIssuedAtToNow(); //设置过期时间,并且小于7天 NumericDate date = NumericDate.now(); date.addSeconds(120*60); claims.setExpirationTime(date); claims.setNotBeforeMinutesInThePast(1); //添加自定义参数,所有值请都使用String类型 //设置消费者标识 claims.setClaim("uid", "11215ac069234abcb8944232b79ae711"); JsonWebSignature jws = new JsonWebSignature(); //设置加密算法 jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256); jws.setKey(new HmacKey(Base64.decode(JsonUtil.parseJson(privateKeyJson).get("k").toString()))); jws.setPayload(claims.toJson()); String jwtResult = jws.getCompactSerialization(); System.out.println("Generate Json Web token , result is \n " + jwtResult); } }
代码相关设置说明:
设置
privateKeyJson
、消费者标识、过期时间,同对称加密算法。设置加密算法,即
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256)
。该加密算法和JWKS保持一致。对于非对称加密算法,要用其私钥进行加密。
... jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyJson)).getPrivateKey(); jws.setKey(privateKey); ...
根据自身业务需要,可以在JWKS的
PAYLOAD
中添加自定义参数。
客户端向网关发送业务请求
目前云原生API网关支持Header的模式传递Token,客户可以自定义请求Header的名字和Token的前缀,请求验证时必须与消费者认证方式配对的key和前缀保持一致。
请求不携带提供JWT,返回401。
curl http://xxx.hello.com/test
请求携带错误JWT,返回401。
curl http://xxx.hello.com/test -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ1'
请求携带JWT,但JWT代表的消费者无权访问API或者路由时,返回403。
# consumer1没有授权给下列路径指定路由或者api curl 'http://xxx.example.com/test' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxODY1NjczODE5fQ.-vBSV0bKeDwQcuS6eeSZN9dLTUnSnZVk8eVCXdooCQ4'
服务端验证Token请求
服务端验证Token主要分为以下三步:
服务端收到用户请求时,先检查是否携带Token,未携带Token的请求,直接拒绝,返回401。
携带Token的请求,使用用户jwks中配置的私钥验证Token是否合法且有效,不合法或者失效,直接拒绝,返回401。
如果Token合法且有效后,再校验其代表的消费者是否授权给正在访问的API或者路由。
常见错误码说明
HTTP 状态码 | 出错信息 | 原因说明 |
401 | Jwt missing | 请求头未提供JWT |
401 | Jwt expired | JWT已经过期 |
401 | Jwt verification fails | JWT payload校验失败,如iss不匹配 |
403 | Access Denied | 无权限访问当前路由 |
AK/SK(HMAC)认证
客户端生成签名
客户端生成签名流程概览:
提取签名串:从原始请求中提取关键数据,得到一个用来签名的字符串。
加密签名:使用加密算法和配置的SK对关键数据签名串进行加密处理,得到签名。
添加签名:将签名所相关的所有头加入到原始HTTP请求中,得到最终HTTP请求。
步骤一:提取签名串
客户端需要从Http请求中提取出关键数据,组合成一个签名串,生成的签名串格式如下:
HTTPMethod
Accept
Content-MD5
Content-Type
Date
Headers
PathAndParameters
以上7个字段构成整个签名串,字段之间使用\n间隔,如果Headers字段为空,则不需要加\n,其他字段如果为空都需要保留\n。签名对大小写敏感。下面介绍每个字段的提取规则:
HTTPMethod:HTTP的方法,全部大写,例如POST。
Accept:请求中的Accept头的值,可为空。建议显式设置Accept Header。当Accept为空时,部分Http客户端会给Accept设置默认值为
*/*
,导致签名校验失败。Content-MD5:请求中的Content-MD5头的值,可为空。只有在请求存在Body且Body为非Form形式时才计算Content-MD5头,下面是Java的Content-MD5值的参考计算方式:
String content-MD5 = Base64.encodeBase64(MD5(bodyStream.getbytes("UTF-8")));
Content-Type:请求中的Content-Type头的值,可为空。
Date:请求中的Date头的值,当未开启
date_offset
配置时,可为空,否则将用于时间偏移校验。Headers:用户可以选取指定的Header参与签名,关于Header的签名串拼接方式有以下规则:
参与签名计算的Header的Key按照字典排序后使用如下方式拼接。
HeaderKey1 + ":" + HeaderValue1 + "\n"\+ HeaderKey2 + ":" + HeaderValue2 + "\n"\+ ... HeaderKeyN + ":" + HeaderValueN + "\n"
某个Header的Value为空,则使用HeaderKey+":"+"\n"参与签名,需要保留Key和英文冒号。
所有参与签名的Header的Key的集合使用英文逗号分割放到Key为X-Ca-Signature-Headers的Header中。
以下Header不参与Header签名计算:X-Ca-Signature、X-Ca-Signature-Headers、Accept、Content-MD5、Content-Type、Date。
PathAndParameters: 这个字段包含Path,Query和Form中的所有参数,具体组织形式如下:
Path + "?" + Key1 + "=" + Value1 + "&" + Key2 + "=" + Value2 + ... "&" + KeyN + "=" + ValueN
重要Query和Form参数对的Key按照字典排序后使用上面的方式拼接。
Query和Form参数为空时,则直接使用Path,不需要添加
?
。参数的Value为空时只保留Key参与签名,等号不需要再加入签名。
Query和Form存在数组参数时(key相同,value不同的参数) ,取第一个Value参与签名计算。
步骤二:加密签名
客户端从HTTP请求中提取出关键数据组装成签名串后,需要对签名串进行加密及编码处理,形成最终的签名。具体的加密形式如下,其中 stringToSign
为提取出来的签名串,secret
为AK/SK认证身份配置中的SK,sign
为最终生成的签名:
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
byte[] secretBytes = secret.getBytes("UTF-8");
hmacSha256.init(new SecretKeySpec(secretBytes, 0, secretBytes.length, "HmacSHA256"));
byte[] result = hmacSha256.doFinal(stringToSign.getBytes("UTF-8"));
String sign = Base64.encodeBase64String(result);
总结:将 stringToSign
使用UTF-8解码后得到Byte数组,然后使用加密算法对Byte数组进行加密,使用Base64算法进行编码,形成最终的签名。
步骤三:添加签名
客户端需要将以下四个Header放在HTTP请求中传输给API网关,进行签名校验:
x-ca-key:取值为AK/SK认证身份配置中的AK。
x-ca-signature-method:签名算法,取值HmacSHA256或者HmacSHA1,可选,默认值为HmacSHA256。
x-ca-signature-headers:所有签名头的Key的集合,使用英文逗号分隔,可选。
x-ca-signature:签名,必选。
服务端签名验证
服务器验证客户端签名概览:
提取签名串:从接收到的请求中提取关键数据,得到一个用来签名的字符串。
提取AK:从接收到的请求中读取AK ,通过AK 查询到对应的SK。
计算签名:使用加密算法和SK对关键数据签名串进行加密处理,得到签名。
签名验证:从接收到的请求中读取客户端签名,对比服务器端签名和客户端签名的一致性。
异常处理
网关签名校验失败时,会将服务端的签名串(StringToSign)放到HTTP Response的Header中返回到客户端,Key为:X-Ca-Error-Message,用户只需要将本地计算的签名串(StringToSign)与服务端返回的签名串进行对比即可找到问题。如果服务端与客户端的StringToSign一致,请检查用于签名计算的AK/SK认证身份中的SK是否正确。因为HTTP Header中无法表示换行,因此StringToSign中的换行符都被替换成#
,如下所示:
X-Ca-Error-Message: Server StringToSign:`GET#application/json##application/json##X-Ca-Key:200000#X-Ca-Timestamp:1589458000000#/app/v1/config/keys?keys=TEST`
相关错误码
HTTP 状态码 | 出错信息 | 原因说明 |
401 | Invalid Key | 请求头未提供x-ca-key,或者x-ca-key无效。 |
401 | Empty Signature | 请求头未提供x-ca-signature签名串。 |
400 | Invalid Signature | 请求头x-ca-signature签名串,与服务端计算得到签名不一致。 |
400 | Invalid Content-MD5 | 请求头content-md5不正确。 |
400 | Invalid Date | 根据请求头date计算时间偏移超过配置的 date_offset。 |
413 | Request Body Too Large | 请求Body超过限制大小:32 MB。 |
413 | Payload Too Large | 请求Body超过全局配置DownstreamConnectionBufferLimits。 |
403 | Unauthorized Consumer | 请求的调用方无访问权限。 |
API Key认证
按照设置的凭证来源方式来验证请求,API与路由请求类似,下面以路由为例:
API Key的凭证来源主要有三类:
默认凭证来源:Authorization: <Bearer> Token。
自定义Header,填写Header参数名。
自定义Query参数,填写query参数名。
默认凭证来源
假设以下请求会匹配到这条路由 abc ,将API Key设置在http特定请求头(Authorization)中,并为API Key追加前缀Bearer,留意Bearer与API Key中间留有空格。
curl http://xxx.test.com/test -H 'x-api-key: Bearer 2bda943c-ba2b-11ec-ba07-00163e1250b5'
将API Key设置在http请求头中
curl http://xxx.test.com/test -H 'x-api-key: Bearer 2bda943c-ba2b-11ec-ba07-00163e1250b5'
自定义header来源
假设以下请求会匹配到这条路由 abc ,API Key 设置在 http 请求头中。
如果路由未开启消费者认证鉴权策略,则拒绝访问,提示401。
curl http://xxx.test.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
如果路由已开启消费者认证鉴权策略,但未授权,则提示403。
curl http://xxx.test.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5'
自定义Query参数
假设以下请求会匹配到这条路由 abc ,将 API Key 设置在 url 参数中。
如果路由未开启消费者认证鉴权策略,则拒绝访问,提示401。
curl http://xxx.test.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
如果路由已开启消费者认证鉴权策略,但未授权,则提示403。
curl http://xxx.test.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
相关错误码
HTTP 状态码 | 出错信息 | 原因说明 |
401 | Request denied by Key Auth check. Muti API key found in request. | 请求提供多个 API Key。 |
401 | Request denied by Key Auth check. No API key found in request. | 请求未提供 API Key。 |
401 | Request denied by Key Auth check. Invalid API key. | 不允许当前 API Key 访问。 |
403 | Request denied by Key Auth check. Unauthorized consumer. | 请求的调用方无访问权限。 |