客户端凭证模式(M2M)

客户端模式Client Credential主要用于一个应用后台调用另一个应用系统接口API资源,调用方在调用API资源前,需要先进行接口鉴权,如根据Client ID和Client Secret获取到令牌Access Token,然后调用方再携带令牌访API资源。

客户端模式非常适应于Machine-to-Machine(M2M)模式,比如通过后台服务发起应用的认证,而不是用户。

在M2M模式中,全程没有用户参与,仅在应用系统之间进行的授权操作。这种模式适用于需要机器之间进行自动化身份验证和授权的场景,如后台服务、API调用和数据同步等。

本文档介绍在竹云IDaaS平台中注册API消费者应用和API资源(API提供者),并对API消费者应用需要访问的API资源授权后,API消费者应用如何安全访问API资源的整体过程。

# 术语说明

M2M模式:Machine-to-Machine(机器对机器),简称M2M。

API消费者应用:作为API消费者(也叫做M2M应用),需要访问API资源来获取相关业务数据。

API资源:作为API提供者,通过开放API接口供API消费者进行调用。

# 授权流程

M2M模式流程:

  1. API消费者应用(M2M应用)使用Client ID、Client Secret向竹云IDaaS发起授权请求,竹云IDaaS验证Client ID、Client Secret通过后,返回颁发的Access Toke给API消费者应用。
  2. API消费者应用携带Access Token访问API资源,API资源服务器验证Access Token合法性、验证权限Scope列表是否在授权范围内。
  3. 资源服务器验证Access Token和权限Scope通过后, 返回API资源的详细信息。

# 开发步骤

竹云IDaaS平台采用 Oauth2.0 M2M模式接入开发流程如下:

# 注册API消费者应用

  1. 登录竹云IDaaS企业中心,选择导航栏资源 > 应用菜单进入应用列表,点击添加自建应用按钮,填写应用名称后点击保存按钮创建API消费者应用,点击进入应用详情链接进入API消费者应用详情页面。
  2. 在应用详情界面点击ClientSecret右侧的启用链接获取密钥,复制并保存平台分配的Client ID和Client Secret。 注意:竹云IDaaS系统不储存ClientSecret;获取到密钥后,请您妥善保管。

# 注册API资源(API提供者)并添加权限

  1. 登录竹云IDaaS企业中心,选择导航栏资源 > 企业API菜单进入API产品列表,点击添加自定义API产品按钮,输入产品logo、产品名称、产品API标识后点击保存。 注意:API消费者应用获取IDaaS平台Token时,需要传递参数API资源定义的标识符,竹云IDaaS根据传递的产品API标识查找对应的API资源。

  2. 进入企业API详情界面,点击权限信息进入API权限配置界面,点击右侧添加按钮添加,输入权限代码、权限描述后点击保存。

# 授权API消费者应用并分配权限Scope

  1. 在API资源(API提供者)详情配置界面中,点击应用授权进入API应用授权界面,授权需要访问该API资源的消费者应用。
  2. 选择创建的“API消费者应用”, 点击进入应用配置界面。点击API权限菜单打开权限配置界面,选择已授权访问的API资源后,并分配所需的权限Scope。

# API消费者应用获取Token令牌

API消费者应用携带Client ID、Client Secret参数调用竹云IDaaS获取Token接口,竹云IDaaS验证通过后,返回Access Token和权限Scope列表。

# 请求说明

POST https://{your_domain}/api/v2/oauth2/token

# 请求头

参数名 中文名称 必须 示例值 描述
Authorization 认证信息 必须 Basic UnFCMkhKdNOWk9xWA== 使用client_id和client_secret进行basic64认证,
格式为: base64(client_id:client_secret)
Content-Type 数据类型 必须 application/x-www-form-urlencoded 使用表单方式提交参数

# 请求示例

POST https://{your_domain}/api/v2/oauth2/token

Authorization: Basic UnFCMkhKdGt6bFU...aT0NObkk4NlNOWk9xWA==

Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&audience=https://apiprovider.com

# 请求参数

请求参数 是否必填 类型 说明
grant_type String 固定值:client_credentials
client_id String IDaaS分配给API消费者应用Client ID
client_secret String IDaaS分配给API消费者应用Client Secret
audience String 注册API资源时填写的标识符,推荐使用URL

# 返回示例

正确返回示例
HTTP Status: 200 OK
{
     "access_token": "eyJraWQiOiI3Yzc2ZWYxZTY0OWY0Yjc1OGVkZTczNGQ4ZDY4OWI5OSIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJodHRwczovL2JhbWJvb2Nsb3VkLmlkYWFzLXRlc3QtYWxwaGEuYmNjYXN0bGUuY29tL2FwaS92MS9vYXV0aDIiLCJhdWQiOiJtMm1hcGkiLCJleHAiOjE2ODc3NzM2OTEsImp0aSI6IlJGa01wWmFWbTQ5R3cyS1hHX2s0cFEiLCJpYXQiOjE2ODc3NzE4OTEsIm5iZiI6MTY4Nzc3MTc3MSwic3ViIjoiME1LZzFlM3dvaEJDc21Tck5Xbk80NjZHOTJ1Q0JmbEQiLCJhenAiOiIwTUtnMWUzd29oQkNzbVNyTlduTzQ2Nkc5MnVDQmZsRCJ9.TIL1WjzqRYdamTgIF591hTq8J08-PrZBRRDvxu9q88wLd5eHjEwfuamGQ2PmdMPXzJy7JCqX8Odr4Kpqlh04jwLYUv1vfIzApM2xjmd8MxU73uG9659PSKyf1yoP9_TLhDd30mgXLN2Fc7IgT1MAnQVTNYmlGU_JrRf-ECE44hMExDcGLScZF7xjJsWjAVX7Wzg4YiVTor3v4oGHdI2-NiEHMdOn2pIvWC_5mxCvIoVRWfYVcrRkpEkyBcWqnhNf422SMDitwkSBkVh73r1-zHOsGLUtci6zbaS2jWjN7OE1tA4iniHsgsx0HyzmfGGo9hLkD6kUpsawzjJH5uqSeg",
    "token_type": "Bearer"
}

非法的客户端凭证错误示例
HTTP Status: 401
{
    "error": "invalid_client",
    "error_description": "Bad client credentials"
}
1
2
3
4
5
6
7
8
9
10
11
12
13

更多返回的错误码,请查看Oauth2.0协议错误码 (opens new window)

# 返回参数

返回参数 类型 说明
access_token String IDaaS颁发给消费者应用的令牌
token_type String token类型,默认为Bearer Token

# API消费者应用访问API资源

API消费者应用访问API资源时,请求头中需携带竹云IDaaS颁发的Access Token,请求头示例如:

Authorization:Bearer eyJraWQilJTMjU2In0.eyJpiOisRCJ9.TIL1WjwzjJH5uqSeg
1

注意:示例中请求头中的Access Token出于美观被格式化。

# API资源服务器(API提供者)验证Token

API资源服务器收到请求资源时,需校验Access Token和权限Scope,验证通过后返回给API消费者应用相关资源数据。

# Token示例说明

竹云IDaaS采用标准Oauth2.0协议颁布授权的Token令牌,Token令牌采用标准的JWT(JSON Web Token)进行封装, 拆包解码后的Access Token参数内容如下

{
	"iss": "https://{yourdomain}/api/v1/oauth2",
	"aud": "https://apiprovider.com",
	"exp": 1687775036,
	"jti": "sFZ-WBf2fj6zHvPxb6k12w",
	"iat": 1687773236,
	"nbf": 1687773116,
	"sub": "0MKg1e3wohBCsmSrNWnO466G92uCBflD",
	"scope": "add read delete",
	"azp": "0MKg1e3wohBCsmSrNWnO466G92uCBflD"
}
1
2
3
4
5
6
7
8
9
10
11

# Token参数说明

参数名 类型 说明
iss String Token签发者
aud String API资源(API提供者)定义的标识符
exp String Token过期时间点
jti String Token唯一标识
iat String Token签发时间点
nbf String Token生效时间点
sub String IDaaS分配给API消费者应用的Client ID
scope String API权限Scope列表,多个权限列表之间使用空格分隔;若无权限时,不返回该参数。
azp String IDaaS分配给API消费者应用的Client ID

# 验证Token

API资源服务器需要验证Token合法性和权限Scope,验证的步骤包含:

  1. 使用IDaaS公钥证书对Token进行验签;
  2. 验证Token中issuer参数是否合法;
  3. 验证Token中audience参数是否合法;
  4. 验证Token是否过期或失效;
  5. 验证权限Scope列表是否在授权范围内;
  6. 验证其它自定义参数......

以下是java语言使用JWT公钥验证id_token示例代码

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
import org.jose4j.keys.resolvers.VerificationKeyResolver;

public class JwtVerificationExample {

    public static void main(String[] args){
        
        try {
            //竹云IDaaS颁发的token
            String idToken = "replace your token";
            //竹云IDaaS JWT keys endpoint
            String jwks_uri = "https://{your_domain}/api/v1/oauth2/keys";
            //token颁发标识
            String issuer = "https://{your_domain}/api/v1/oauth2";
            //API资源定义的标识符
            String audience = "replace your api identifier";
            VerificationKeyResolver verificationKeyResolver = new HttpsJwksVerificationKeyResolver(new HttpsJwks(jwks_uri));
            JwtConsumer consumer = new JwtConsumerBuilder().setVerificationKeyResolver(verificationKeyResolver)
                    .setRequireExpirationTime()
                    .setAllowedClockSkewInSeconds(300)
                    .setRequireSubject()
                    .setExpectedIssuer(issuer)
                    .setExpectedAudience(audience)
                    .build();
            JwtClaims claims = consumer.processToClaims(idToken);
            Map<String, Object> claimsMap = claims.getClaimsMap();
            //获取API权限
            Object scope = claimsMap.get("scope");
            if (scope != null){
                String[] apiArray = StringUtils.split((String) scope, " ");
                //校验api权限是否满足,省略......
            }
        } catch (Exception e) {
            //token验证失败处理
        }
    }
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46