更新时间:2024-07-11 GMT+08:00
分享

同步第三方认证协议用户

使用说明

AstroZero支持通过自定义认证,配置AstroZero对接第三方应用认证的必要信息,即配置账号级通用IAM对接。

操作步骤

  1. 参考登录管理中心中操作,登录AstroZero管理中心。
  2. 在左侧导航栏中,选择“系统管理 > 统一身份认证”,单击“自定义认证”。
  3. 在“基本配置”中,输入标签和名称后,单击“下一步”。
  4. 在“认证源配置”中,设置认证源信息,单击“下一步”。

    表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”获取方法如下,该方法创建的证书通常只适用于测试场景。

    1. 在Linux机器上执行如下命令,生成密钥文件“saml.key”。
      openssl genrsa -out saml.key 2048

      命令执行后将在当前目录生成一个“saml.key”的私钥文件。生成的密钥格式必须为:

      -----BEGIN PRIVATE KEY-----
      ……………………………………………..
      -----END PRIVATE KEY-----
    2. 用上一步的密钥文件去签发,生成文件“saml.csr”。
      openssl req -new -key saml.key -out saml.csr
    3. 根据提示依次输入国家、地区、组织、邮箱及common name(用户名或域名,如果为https申请,该参数值必须和域名相同,否则会引发浏览器警报),按回车键确认。
    4. 执行如下命令,使用之前生成的文件去签发生成自己的证书文件“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”属性对应的字段是必填的。

    图1 Oauth认证模式
    图2 CAS认证模式

  5. 在“身份源配置”中,配置对端鉴权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;
        }
    }

  6. 单击“去同步”,同步组织机构和用户信息,支持立即同步和设置定时同步任务两种方式。

    图3 同步组织机构和用户信息
    • 立即同步
      1. 单击“同步部门”下的“立即同步”,在弹框中,若勾选“同时清除未包含在同步中的部门”表示在AstroZero中清除第三方中已删除的部门,单击“确定”。
      2. 单击“同步用户”下的“立即同步”,在弹框中,勾选“同时清除未包含在同步中的用户”表示在AstroZero中清除第三方中已删除的用户,单击“确定”。
    • 设置定时任务实现自动同步
      1. 在“同步部门”后,打开“开启自动同步”开关。单击“同步部门”下的“自动同步设置”,设置首次同步时间、执行周期,勾选“同时清除未包含在同步中的部门”表示在AstroZero中清除第三方中已删除的部门,单击“确定”。
      2. 在“同步用户”后,打开“开启自动同步”开关。单击“同步用户”下的“自动同步设置”,设置首次同步时间、执行周期,勾选“同时清除未包含在同步中的用户”表示在AstroZero中清除第三方中已删除的用户,单击“确定”。
        图4 开启部门的自动同步

    系统会同步第三方机构组织信息到AstroZero管理中心的“用户管理 > 角色”中,并同步第三方用户信息到AstroZero管理中心的“用户管理 > 用户”中。

  7. (可选)在“认证配置”菜单中,单击“下载”,下载SP Metadata文件。

    • 后续需要配置该文件到身份提供商中。
    • 4中认证协议选择“SAML2”时,才需要执行此步骤。
    图5 下载SP Metadata文件

相关文档