中大型企业通常都使用自研的IAM(Identity and Access Management)系统,来统一纳管所有的员工账号、组织架构的信息, 包括员工生命周期的管理, 比如入离职、转岗等.
而业务系统往往也需要这部分数据, 惯用的一种做法是业务系统通过调用IAM的API, 周期性的从IAM中同步数据. 但因为这里缺少统一的API协议规范, 经常需要定制化开发. 基于此背景, 本文尝试定义一个协议, 来规范化业务系统和IAM系统的数据同步API.
该协议为"客户端+服务端"模型, 数据提供方(通常是IAM)作为服务端来实现这个协议, 数据使用方(通常是业务系统)作为客户端来按照协议来发起API调用.
- 凭证信息: client_id + client_secret
IAM系统给业务系统颁发的凭证信息, 业务系统通过"凭证信息"来换取接口调用时必须的"鉴权access_token"
- 鉴权access_token
业务系统调用同步接口的鉴权信息
- 公开配置信息: 又称well-known
well-known相当于一个公开的注册表, 里面包含了各个接口的请求地址, 主要包括:
- token_endpoint: 请求鉴权access_token的接口地址, 必须实现
- list_department_endpoint: 获取部门列表的接口地址, 必须实现
- list_deptartment_users_endpoint: 获取部门下用户成员详情的接口地址, 必须实现
- search_department_endpoint: 部门搜索接口, 可选实现
- search_user_endpoint: 用户搜索接口, 可选实现
- list_group_endpoint: 获取group列表的接口地址, 可选实现
- list_group_users_endpoint: 获取group下用户id的接口地址, 可选实现
- search_group_endpoint: group搜索接口, 可选实现
- 其他: 如无特殊说明, 所有API都遵循REST风格的定义, 包括
- 接口鉴权方式:
- Bearer Token的方式, 即Authorization: Bearer xxxxx-token
- 若请求token过期或无效,接口需要返回http status为401
- content-type为application/json
- 数据结构一律为snake_case风格
- 分页请求均遵循"分页请求"中的定义(如下)
- 业务侧返回的error均遵循"通用错误返回"中的定义(如下)
- 接口鉴权方式:
作为客户端, 会按照如下约定进行数据同步.
- 前置条件: 需要从数据提供方获取
- 凭证信息(client_id + client_secret)
- well-known接口地址
- 解析well-known配置: 调用well-known接口来获取"公开配置信息"
- 获取鉴权access_token: 调用token_endpoint接口来获取(为了提升性能,建议缓存)
- 数据同步顺序:
- 获取部门列表: 分页循环获取
- 获取group列表: 分页循环获取
- 获取group用户id列表: 依次遍历上面获取的group id, 分页、循环获取group下用户id
- 获取用户列表: 依次遍历上面获取的部门id, 分页、循环获取部门下直属用户详情数据
作为服务端, 需要实现下述的几个API.
- 接口鉴权方式: 不需要鉴权, 公开接口
- 请求方式: GET
- 参数说明: 不需要
- 返回字段说明:
字段名 类型 说明 spec string 协议版本号, 固定为v1 token_endpoint url 获取access_token的接口地址 list_department_endpoint url 获取部门列表的接口地址 list_deptartment_users_endpoint url 获取部门直属成员详情的接口地址 search_department_endpoint url 搜索部门的接口地址 search_user_endpoint url 搜索用户的接口地址 list_group_endpoint url 获取group列表的接口地址 list_group_users_endpoint url 获取group下用户id列表的接口地址 search_group_endpoint url 搜索group的接口地址 - 返回示例:
{ "spec": "v1", "token_endpoint": "https://example.com/v1/token", "list_department_endpoint": "http://example.com/v1/depts", "list_deptartment_users_endpoint": "https://example.com/v1/users", "search_department_endpoint": "http://example.com/v1/depts:search", "search_user_endpoint": "http://example.com/v1/users:search", "list_group_endpoint": "http://example.com/v1/groups", "list_group_users_endpoint": "https://example.com/v1/groups:users", "search_group_endpoint": "http://example.com/v1/groups:search", }
- 接口鉴权方式: 不需要
- 请求方式: POST
- 参数说明: body的方式传入
字段名 类型 说明 grant_type string 固定为client_credentials client_id string 凭证信息中的client_id client_secret string 凭证信息中的client_secret - 返回字段说明:
字段名 类型 说明 token_type string token类型,固定为Bearer access_token string 实际返回的access_token的值 expires_in int token的有效期, 单位为秒, 比如7200表示2小时. 调用者可以根据有效期来缓存token. 注: 允许数据提供方提前过期access_token,调用方需要主动去请求个新的access_token并重放本次请求 - 返回示例:
{ "token_type": "Bearer", "access_token": "xxxxxxxxxxxxx-access-token", "expires_in": 1800 }
- 错误返回: 当颁发access_token失败时, http status返回400或401, 常见错误返回包括
http status code 说明 400 invalid_request 比如入参缺少client_id或client_secret 401 invalid_client 比如client_id/client_secret校验失败
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递分页参数
字段名 类型 说明 cursor string 分页请求的游标, 初始请求为"" size int 分页大小, 最大支持100, 若传入值>100,则当做50来处理 - 返回字段说明:
字段名 类型 说明 has_next bool 是否还有数据未返回 cursor string 分页标记,当has_next为true时,同时返回下一次分页请求的标记. 当has_next为false时,不需要返回 data []department 返回的部门数据, 部门的数据结构参考下面定义 - 部门数据结构说明
字段名 类型 说明 id string 部门不可变的唯一标识, 长度<=64, 必须返回 name string 部门名称,长度<=128, 必须返回 parent string 父部门唯一标识. 若为根部门则返回"", 必须返回 order int 部门在其同级部门的展示顺序, 可不返回 - 成功返回示例:
{ "has_next": true, "cursor": "xxxx-cursor", "data": [ {"id": "1", "parent": "", "name": "中国", "order": 0}, {"id": "1.1", "parent": "1", "name": "北京", "order": 0}, {"id": "1.2", "parent": "1", "name": "上海", "order": 0}, {"id": "1.3", "parent": "1", "name": "辽宁", "order": 0}, {"id": "1.1.1", "parent": "1.1", "name": "朝阳", "order": 0} ] }
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递参数
字段名 类型 说明 id string 部门唯一标识 cursor string 分页请求的游标, 初始请求为"" size int 分页大小, 最大支持100 - 返回字段说明:
字段名 类型 说明 has_next bool 是否还有数据未返回 cursor string 分页标记,当has_next为true时,同时返回下一次分页请求的标记. 当has_next为false时,不需要返回 data []user 返回的部门直属用户详情数据, 用户的数据结构参考下面定义 - 用户详情数据结构说明
字段名 类型 说明 id string 用户不可变的唯一标识, 长度<=64,必须返回 name string 显示名,长度<=64,必须返回 username string 登录名,唯一,长度<=64,可不返回, 建议username/email/mobile至少返回1个 email string 邮箱,唯一,长度<=128,可不返回, username/email/mobile至少返回1个 mobile string 手机号,唯一,可不返回,需遵循E.164格式,比如+8613411112222 position string 职务,长度<=64,可不返回 employee_number string 工号,长度<=64,可不返回 join_time timestamp 入职时间戳(unix timestamp),可不返回 active bool 用户状态, false:禁用, true:启用 avatar url 头像url,可不返回 main_department string 用户所属主部门唯一标识, 必须返回 other_departments []string 用户所属副主部门唯一标识,可不返回 order int 部门在其主部门下的展示顺序,可不返回 extattrs map 其他属性,以key-value的形式存在 - 成功返回示例:
{ "has_next": false, "cursor": "", "data": [ { "id": "uid-2", "name": "user 2", "username": "user2", "email": "user2@example.com", "mobile": "+8613411112222", "position": "developer", "employee_number": "22345", "join_time": 1719935216, "avatar": "https://example.com/avatar/uid-2.png", "status": 1, "main_department": "1.1", "other_departments": null, "order": 8, "extattrs": {"age": 20} }, { "id": "uid-2.1", "name": "user 2.1", "username": "user2.1", "email": "user2.1@example.com", "mobile": "+8613411113333", "position": "qa", "employee_number": "12345", "join_time": 1719935216, "avatar": "https://example.com/avatar/uid-2.1.png", "status": 1, "main_department": "1.1", "other_departments": ["1.2"], "order": 5, "extattrs": {"age": 30} } ] }
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递参数.
字段名 类型 说明 keyword string 搜索关键字 - 返回说明:
字段名 类型 说明 data []department 返回的部门数据, 部门的数据结构同上述部门定义. 若没有匹配的部门,则接口应该返回200, 返回data为空 - 成功返回示例:
{ "data": [ {"id": "1.1.1", "parent": "1.1", "name": "朝阳", "order": 0}, {"id": "1.3.1", "parent": "1.3", "name": "朝阳", "order": 0} ] }
- 接口实现建议:
- 最多返回10个匹配部门
- 搜索关键字支持根据部门名称做模糊匹配, 或支持根据id进行过滤
- 若无匹配部门,接口不要报错, 而是正常返回, 但返回的data为空.
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递参数.
字段名 类型 说明 keyword string 搜索关键字 - 返回说明:
字段名 类型 说明 data []user 返回的用户详情数据, 用户的数据结构同上述部门定义. 若没有匹配的用户,则接口应该返回200, 返回data为空 - 成功返回示例:
{ "data": [ { "id": "uid-2.1", "name": "user 2.1", "username": "user2.1", "email": "user2.1@example.com", "mobile": "+8613411113333", "position": "qa", "employee_number": "12345", "join_time": 1719935216, "avatar": "https://example.com/avatar/uid-2.1.png", "active": true, "main_department": "1.1", "other_departments": ["1.2"], "order": 5, "extattrs": {"age": 30} } ] }
- 接口实现建议:
- 最多返回10个匹配用户
- 搜索关键字支持根据用户名称做模糊匹配, 或支持根据id、登录名、邮箱、手机号进行过滤
- 若无匹配用户,接口不要报错, 而是正常返回, 但返回的data为空.
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递分页参数
字段名 类型 说明 cursor string 分页请求的游标, 初始请求为"" size int 分页大小, 最大支持100, 若传入值>100,则当做50来处理 - 返回字段说明:
字段名 类型 说明 has_next bool 是否还有数据未返回 cursor string 分页标记,当has_next为true时,同时返回下一次分页请求的标记. 当has_next为false时,不需要返回 data []group 返回的group数据, group的数据结构参考下面定义 - group数据结构说明
字段名 类型 说明 id string group不可变的唯一标识, 长度<=64, 必须返回 name string group名称, 唯一,长度<=128, 必须返回 - 成功返回示例:
{ "has_next": true, "cursor": "xxxx-cursor", "data": [ {"id": "1", "name": "developer"}, {"id": "2", "name": "qa"}, {"id": "3", "name": "sales"}, {"id": "4", "name": "hr"} ] }
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递参数
字段名 类型 说明 id string group唯一标识 cursor string 分页请求的游标, 初始请求为"" size int 分页大小, 最大支持100 - 返回字段说明:
字段名 类型 说明 has_next bool 是否还有数据未返回 cursor string 分页标记,当has_next为true时,同时返回下一次分页请求的标记. 当has_next为false时,不需要返回 data []string 返回用户的id列表, 用户的数据结构参考上面定义 - 成功返回示例:
{ "has_next": false, "cursor": "", "data": [ "uid-1", "uid-2", "uid-3" ] }
- 错误返回示例: 参见通用的错误返回
- 接口鉴权方式: Bearer Token的方式, 即Authorization: Bearer xxxxx-token, 若请求token过期或无效,接口需要返回http status为401
- 请求方式: GET
- 参数说明: 以query的形式, 传递参数.
字段名 类型 说明 keyword string 搜索关键字 - 返回说明:
字段名 类型 说明 data []group 返回的group数据, group的数据结构同上述group定义. 若没有匹配的group,则接口应该返回200, 返回data为空 - 成功返回示例:
{ "data": [ {"id": "1", "name": "developer"}, {"id": "2", "name": "dev"} ] }
- 接口实现建议:
- 最多返回10个匹配group
- 搜索关键字支持根据group名称做模糊匹配, 或支持根据id进行过滤
- 若无匹配group,接口不要报错, 而是正常返回, 但返回的data为空.
- 错误返回示例: 参见通用的错误返回
- 分页请求参数一律采用游标风格(cursor + size)的分页, 不支持offset方式的分页.
- 分页请求的参数为:
字段名 类型 说明 cursor string 分页请求的游标, 初始请求为"" size int 分页大小, 最大支持100 - 分页请求的返回为:
字段名 类型 说明 has_next bool 是否还有数据未返回 cursor string 分页标记,当has_next为true时,同时返回下一次分页请求的标记. 当has_next为false时,不需要返回 data []object 返回的数据
- 错误返回复用http status作为接口返回的状态码
- 接口业务侧使用通用的数据结构来返回错误
字段名 类型 说明 code string 错误码, 当有错误时,必须返回. 为""时表示请求成功 msg string 错误的描述信息, 建议返回 request_id string 请求的唯一标识, 建议返回 - 返回示例: http 401
{ "code": "invalid_client", "msg": "invalid client id or client secret", "request_id": "zBXaFZpIYrsllcrEjAEBoqIUpuuQFgzq" }
- 常见错误码
http status code 说明 400 invalid_request 比如非法请求,入参缺少client_id或client_secret 401 invalid_client 无效的client_id或client_secret 401 invalid_token 接口调用时access_token校验失败 429 too_many_requests 接口调用超出频率限制
- 单接口的最大QPS限制在50次/秒
- 若数据提供方拦截到请求超出频率限制时, 需要返回http status为429, 业务侧的错误返回码为:
{
"code": "too_many_requests",
"msg": "too many requests",
"request_id": "zBXaFZpIYrsllcrEjAEBoqIUpuuQFgzq"
}
- 业务侧可以在http response header中添加Retry-After来引导调用方重试等待时间, 最长不超过5分钟. 若未指定, 默认为1秒.