同步第三方认证协议用户
使用说明
AstroZero支持通过自定义认证,配置AstroZero对接第三方应用认证的必要信息,即配置账号级通用IAM对接。
操作步骤
- 参考登录管理中心中操作,登录AstroZero管理中心。
- 在左侧导航栏中,选择“系统管理 > 统一身份认证”,单击“自定义认证”。
- 在“基本配置”中,输入标签和名称后,单击“下一步”。
- 在“认证源配置”中,设置认证源信息,单击“下一步”。
表1 认证源配置参数说明 参数
参数说明
认证协议
根据对端认证服务器提供的认证协议进行选择,选择后需要配置相应的地址和凭证信息,目前只支持标准Oauth2、CAS和SAML2协议。
认证地址
认证登录地址,请从第三方获取。
“认证协议”配置为“Oauth2”或“SAML2”时,才需配置此参数。
获取access_token地址
获取access_token的请求地址,请从第三方获取。
“认证协议”配置为“Oauth2”时,才需配置此参数。
获取用户信息地址
获取用户信息的请求地址,请从第三方获取。
“认证协议”配置为“Oauth2”时,才需配置此参数。
access_token参数位置
获取用户信息时,传入access_token的参数位置。
“认证协议”配置为“Oauth2”时,才需配置此参数。
请求方法
调用获取用户信息接口的请求方法。
“认证协议”配置为“Oauth2”时,才需配置此参数。
验证票据地址
验证票据并获取用户属性信息的地址,请从第三方获取。
“认证协议”配置为“CAS”时,才需配置此参数。
登出地址
AstroZero登出之后,跳到服务端的登出地址,使服务端登出,建议配置登出地址。支持在登出地址中添加变量“{redirect}”,登出时替换为当前配置的页面地址。
示例:https://host/api/v1/logout?redirect_url={redirect}
是否登录时自动创建用户
用户首次登录时,如果系统中不存在此用户,是否自动创建用户。
- 是:自动创建用户并登录。
- 否:直接登录失败。
用户权限
当使用第三方用户登录时,如果用户不存在,且开启了自动创建用户,此权限将作为创建用户的默认权限。
是否校验state
建议开启校验state,以防止CSRF攻击。
AstroZero在响应请求后,将重定向到第三方服务器,此时第三方服务器根据该配置决定是否校验state参数一致性(即cookie或session里的state)。如果开启校验后,state不一致,则AstroZero拒绝此请求,且不再发起换取access_token的请求。如果一致,则流程正常运行。
“认证协议”配置为“Oauth2”或“SAML2”时,才需配置此参数。
客户端证书
SAML客户端签名证书公钥。“认证协议”配置为“SAML2”时,才需配置此参数。
自签名证书公钥如“saml.crt”获取方法如下,该方法创建的证书通常只适用于测试场景。
- 在Linux机器上执行如下命令,生成密钥文件“saml.key”。
openssl genrsa -out saml.key 2048
命令执行后将在当前目录生成一个“saml.key”的私钥文件。生成的密钥格式必须为:
-----BEGIN PRIVATE KEY----- …………………………………………….. -----END PRIVATE KEY-----
- 用上一步的密钥文件去签发,生成文件“saml.csr”。
openssl req -new -key saml.key -out saml.csr
- 根据提示依次输入国家、地区、组织、邮箱及common name(用户名或域名,如果为https申请,该参数值必须和域名相同,否则会引发浏览器警报),按回车键确认。
- 执行如下命令,使用之前生成的文件去签发生成自己的证书文件“saml.crt”。
openssl x509 -req -days 365 -in saml.csr -signkey saml.key -out saml.crt
生成的证书格式必须为:
-----BEGIN CERTIFICATE----- …………………………………………………………… -----END CERTIFICATE-----
客户端证书私钥
SAML签名证书私钥。“认证协议”配置为“SAML2”时,才需配置此参数。
自签名证书私钥为生成客户端证书中产生的“saml.key”文件,该方法创建的证书通常只适用于测试场景。
Idp Metadata文件
SAML服务端的元数据文件,需要到SAML身份提供商(Identity Provider,简称IdP)获取,获取方法请参考获取IdP元数据metadata。
“认证协议”配置为“SAML2”时,才需配置此参数。
属性映射
配置AstroZero和第三方的属性映射关系,即映射AstroZero和第三方登录接口返回的用户属性相同的字段。
- Oauth认证模式示例:
请到对端第三方获取用户信息,返回的用户信息如下:
{ "id" : "20201029190841785-CB37-8BD36B...", // 用户ID "name" : "test", // 名称 "userName" : "test", // 用户登录名 "mobile" : "1XXXX456789", // 电话号码 "email" : "123@example.com" // 邮箱 }
获取上述信息后,参照图1,配置第三方字段和AstroZero对应字段的属性映射关系。
- CAS认证模式示例:
请到对端第三方获取用户信息,返回的用户信息如下:
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> <cas:authenticationSuccess> <cas:user>Lily</cas:user> // 用户登录名称 <cas:attributes> <cas:authenticationDate>2020-02-11T09:28:14.987Z</cas:authenticationDate> <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed> <cas:isFromNewLogin>true</cas:isFromNewLogin> <cas:date>2020-02-03T16:00:00.000Z</cas:date> <cas:birthday>2020-02-04T16:00:00.000Z</cas:birthday> <cas:createAt>2020-01-10T09:01:04.000Z</cas:createAt> <cas:disabledAt>null</cas:disabledAt> <cas:disabed>false</cas:disabed> <cas:email>email@example.com</cas:email> // 邮箱 <cas:token>XXXXXXXXXXXXXXXXX</cas:token> // 第三方系统用户token </cas:attributes> </cas:authenticationSuccess> </cas:serviceResponse>
获取上述信息后,参照图2,配置第三方字段和AstroZero对应字段的属性映射关系。
其中,属性“email”和“phone”可以映射为空,当对端没有对应的“name”和“extIdentityID”字段返回时,可以都配置成“userName”属性对应的字段,“userName”属性对应的字段是必填的。
- 在“身份源配置”中,配置对端鉴权ID、对端鉴权密钥和同步脚本,单击“保存”。
同步脚本用于同步第三方机构组织、用户到AstroZero。脚本示例如下,如何创建脚本,请参见脚本开发实例。
// Here's your code./* import * as http from 'http'; import * as db from 'db'; import * as iconv from 'iconv'; import { Encoding } from 'buffer'; export class Output { @action.param({ type: "Any" }) result: any; } export class StructRole { @action.param({ type: "String", description: "组织name,可以对应第三方组织code", isCollection: false }) name: string; @action.param({ type: "String", description: "组织名称,可以对应第三方组织名称", isCollection: false }) label: string; @action.param({ type: "String", description: "上级组织,对应AstroZero组织ID", isCollection: false }) reportTo: string; } export class StructUser { @action.param({ type: "String", description: "用户名称", isCollection: false }) name: string; @action.param({ type: "String", description: "用户账号, 账号内唯一", isCollection: false }) usrName: string; @action.param({ type: "String", description: "第三方系统用户ID, 第三方系统内唯一", isCollection: false }) extIdentityID: string @action.param({ type: "String", description: "所属组织,对应AstroZero内组织ID", isCollection: false }) roleId: string @action.param({ type: "String", description: "创建的业务用户权限, profileId", isCollection: false }) profile: string @action.param({ type: "String", description: "创建的业务用户类型,固定为custom", isCollection: false }) userType: string } @useObject(['PortalUser']) export class SyncContact { @action.method({ input: "Input", output: "Output", description: "开发测试时运行此方法" }) run(): Output { //return this.SyncUser(); return this.SyncRole(); //调试同步组织机构取消注释此行 } // SyncUser为同步用户的方法,方法名不允许修改 SyncUser(): Output { let output = new Output(); let token = this.getToken(); if (!token) { output.result = "同步用户失败"; return output; } let users = this.queryUsers(token); for (let u of users) { let portaluser: StructUser = { name: u.user_name, usrName: u.user_name, extIdentityID: u.user_id, profile: "000T0000000000000003", // 设置为Portal User Profile的ID roleId: "", userType: "custom" } let role = this.queryRoleByName("oneaccess" + u.org_id); if (role) { portaluser.roleId = role.id; } this.addUser(portaluser); } output.result = "同步用户成功"; return output; } // SyncRole为同步组织机构的方法,方法名不允许修改 SyncRole(): Output { let output = new Output(); let token = this.getToken(); if (!token) { output.result = "同步组织机构失败"; return output; } let orgs = this.queryOrgs(token, ""); for (let org of orgs) { this.syncOrg(org, token); } output.result = "同步组织机构成功"; return output; } private addUser(portaluser: StructUser): String { // 校验记录是否已经存在 let option = {}; let condition = { "conjunction": "AND", "conditions": [{ "field": "name", "operator": "eq", "value": portaluser.usrName }] }; let record = db.object('PortalUser').queryByCondition(condition, option); if (record && record.length > 0) { console.log("已存在,不再创建"); return record[0].id; } // 插入数据 let roleId = portaluser.roleId; delete portaluser.roleId; //非portaluser字段,保存后删除 let portaluserId = db.object('PortalUser').insert(portaluser); if (!roleId) { return portaluserId; } db.setup('roleportaluser').insert({ "portaluser": portaluserId, "role": roleId }); return portaluserId; } // 业务自己开发查询逻辑,查询第三方系统的用户,并添加到平台portaluser表, 可以参考帮助文档对接oneaccess示例 private queryUsers(token: string): Array<any> { // 这里写调用三方接口获取用户列表的逻辑 let req: http.Request = { headers: { "content-type": "application/json", "Authorization": "Bearer " + token }, params: { "limit": 100, // 大于100需要做分页遍历查询所有页 "offset": 0, } }; let result = this.sendRequest("https://test.example.com/api/v2/tenant/users", "GET", req); if (result.error_code) { throw new Error(result.error_msg); } return result && result.users || []; } private queryRoleByName(name: string): any { let option = {}; let condition = { "conjunction": "AND", "conditions": [{ "field": "name", "operator": "eq", "value": name }] }; let records = db.setup('role').queryByCondition(condition, option); if (records && records.length > 0) { console.log("已存在,不再创建"); return records[0]; } return null; } private addRole(role: StructRole): string { // 校验记录是否已经存在 let record = this.queryRoleByName(role.name); if (record) { console.log("已存在,不再创建"); return record.id; } // 插入数据 return db.setup('role').insert(role); } // 业务自己开发查询逻辑,查询第三方系统的组织机构逻辑, 并添加到平台role表,可以参考帮助文档对接oneaccess示例,查询首次组织 private queryOrgs(token: string, org_id?: string): Array<any> { // 这里写调用三方接口获取组织结构的逻辑 let level0Orgs = this.getOrgList(100, 0, org_id, token); if (level0Orgs && level0Orgs.organizations) { return level0Orgs.organizations; } return []; } // 同步某一组织,已经组织下子组织 private syncOrg(org: any, token: string, reportTo?: string) { console.log(org) let role = new StructRole(); role.name = "oneaccess" + org.org_id; role.label = org.name; console.log(reportTo) if (reportTo) { role.reportTo = reportTo; } let parentRoleId = this.addRole(role) let childOrgs = this.queryOrgs(token, org.org_id); console.log(childOrgs); for (let childOrg of childOrgs) { if (childOrg.parent_id === org.org_id) { this.syncOrg(childOrg, token, parentRoleId) } } } // oneaccess查询组织机构的方法,对接其他第三方需要重写 private getOrgList(limit: number, offset: number, org_id: string, token: string): any { // 这里写调用三方接口获取组织结构的逻辑 let req: http.Request = { headers: { "content-type": "application/json", "Authorization": "Bearer " + token }, params: { "limit": limit, "offset": offset, "org_id": org_id, "all_child": false } }; let result = this.sendRequest("https://test.example.com/api/v2/tenant/organizations", "GET", req); if (result.error_code) { throw new Error(result.error_msg); } return result; } private sendRequest(url: string, method: string, callOptions?: http.Request): any { let client = http.newClient(); let resp; if (method === "GET") { resp = client.get(url, callOptions); } else { resp = client.post(url, callOptions); } let result = iconv.decode(resp.data, Encoding.Utf8); return JSON.parse(result); } // 获取第三方系统的接口调用凭证,需要业务根据第三方接口适配开发 private getToken(): string { let req = { headers: { 'Content-Type': "application/x-www-form-urlencoded" }, params: { "client_id": "o8XvdwMF7******KLxVzoxarXr", "client_secret": "uf8vBFR1RMAf******Uq31NThCzT1OOAlepsM00", "grant_type": "client_credentials", } }; let result = this.sendRequest('https://test.example.com/api/v2/tenant/token', "POST", req); if (result.error) { throw new Error(result.error); } return result.access_token; } }
- 单击“去同步”,同步组织机构和用户信息,支持立即同步和设置定时同步任务两种方式。
图3 同步组织机构和用户信息
- 立即同步
- 单击“同步部门”下的“立即同步”,在弹框中,若勾选“同时清除未包含在同步中的部门”表示在AstroZero中清除第三方中已删除的部门,单击“确定”。
- 单击“同步用户”下的“立即同步”,在弹框中,勾选“同时清除未包含在同步中的用户”表示在AstroZero中清除第三方中已删除的用户,单击“确定”。
- 设置定时任务实现自动同步
- 在“同步部门”后,打开“开启自动同步”开关。单击“同步部门”下的“自动同步设置”,设置首次同步时间、执行周期,勾选“同时清除未包含在同步中的部门”表示在AstroZero中清除第三方中已删除的部门,单击“确定”。
- 在“同步用户”后,打开“开启自动同步”开关。单击“同步用户”下的“自动同步设置”,设置首次同步时间、执行周期,勾选“同时清除未包含在同步中的用户”表示在AstroZero中清除第三方中已删除的用户,单击“确定”。
图4 开启部门的自动同步
系统会同步第三方机构组织信息到AstroZero管理中心的“用户管理 > 角色”中,并同步第三方用户信息到AstroZero管理中心的“用户管理 > 用户”中。
- 立即同步
- (可选)在“认证配置”菜单中,单击“下载”,下载SP Metadata文件。
- 后续需要配置该文件到身份提供商中。
- 当4中认证协议选择“SAML2”时,才需要执行此步骤。
图5 下载SP Metadata文件