飞书组织架构同步(部门和用户) Java-API 的使用

Posted wxhyc2019

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了飞书组织架构同步(部门和用户) Java-API 的使用相关的知识,希望对你有一定的参考价值。

飞书组织架构同步(部门和用户) Java-API使用

概述

1.飞书JavaSDK

飞书开放接口JavaSDK是飞书官方开发的一套封装了飞书开放平台请求的Java借口,便于企业应用整合到飞书平台。
具体可参考飞书官方开源仓库(可以拉取源码,查看官方demo):

https://github.com/larksuite/oapi-sdk-java
	建议直接查看对应Service源码,官方demo过于简单(小声吐槽)。


本次实现主要针对飞书平台的基础通讯录功能(ContantService)的使用

1.准备工作

1.1 导入maven依赖

这里使用的是1.0.17-rc2版本,自己开发可以选用更新的版本。

     <dependency>
            <groupId>com.larksuite.oapi</groupId>
            <artifactId>larksuite-oapi</artifactId>
            <version>1.0.17-rc2</version>
        </dependency>

1.2 功能概述

  1. 需要实现的功能: 将本地的部门和人员同步至飞书开放平台
  2. 主要涉及的API:通讯录功能 即 ContactService
  3. Constants.CUSTOM_DEPT_ID_TYPE = “department_id” 含义为:设置飞书部门id为自定义id(默认为飞书自动生成的open_id)
  4. Constants.CUSTOM_USER_ID_TYPE = “user_id” 含义为:设置飞书用户id为自定义id(默认飞书自动生成的open_id)

1.3 解决思路

1.本地建立两张飞书信息表(feishu_user和feishu_dept)
2.对线上飞书平台的操作返回成功(response.getCode == 0)后,将信息都存入本地飞书表
3.比对飞书信息表和本地架构信息表,查找出需要 增、删、改 的部门和用户
4.批量操作同步数据

1.4 设置ContactService

在创建ContactService时,SDK会自动进行access_token的封装,实现开箱即用。
只要传入app_id和app_secret即可获取通讯录对应的权限。

@Configuration
public class FeishuConfig 
	//app_id和app_secret在飞书平台的开发者后台-企业自建应用-凭证与基础信息可找到
    private static final AppSettings appSettings = Config
    .createInternalAppSettings(Constants.APP_ID,Constants.APP_SECRET,null,null);
    //要使用时直接使用@Resource注解即可自动注入
    @Bean(name = "contactService")
    public ContactService getContactService()
        final Config config = new Config(Domain.FeiShu, appSettings,new DefaultStore());
        return new ContactService(config);
    



2. 部门对接

2.1 部门的创建

注意事项: 必须先创建父部门(本地oa部门一般都有部门层级字段,按层级排序即可),然后才能创建子部门

		Department department = new Department();//Department为API中封装好的实体类
		//对department的必要参数进行设置
		//略,具体请查询官方API文档(不可设置leaderUserId,因为leader可能没有创建)
		//顶级部门的parentDepartmentId为“0”
        Response<DepartmentCreateResult> response = contactService.getDepartments()
                .create(department)
                .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)//使用自己设置的department_id
                .setUserIdType(Constants.CUSTOM_USER_ID_TYPE)//使用自己设置的user_id
                .execute();
        if(response.getCode() == 0)
            //如果创建成功,将飞书部门信息录入本地
            maaper.insert();//略
            System.out.println("更新成功部门号"+dept.getDepartmentId());
        else
            System.out.println("更新失败部门号"+dept.getDepartmentId()+response.getMsg());
        

2.2 部门的更新

		Department dept = new Department();
		//设置dept的必要参数
		//略
		//只有更新部门时才设置leaderId,因为创建部门时leader可能还没有被创建
		dept.setLeaderUserId(leaderId);
        Response<DepartmentUpdateResult> response = contactService.getDepartments()
        		.update(dept)
                .setDepartmentId(dept.getDepartmentId())
                .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)
                .setUserIdType(Constants.CUSTOM_USER_ID_TYPE).execute();
        if(response.getCode() == 0)
        	//更新本地的飞书信息
        	System.out.println("更新成功部门号"+dept.getDepartmentId());
       	else
           	System.out.println("更新失败部门号"+dept.getDepartmentId()+response.getMsg());
   		

2.3 部门的删除(递归删除)

飞书对于部门的删除要求较为严格,必须保证部门中没有子部门且没有用户
在数据同步过程中,必须先删除用户,再递归删除空部门(没有用户的部门)。

2.3.1子部门的查询

	/**
	*	查询某个部门的直属子部门
	*/
    public List<String> selectSubDept(String deptId) throws Exception 
        Response<DepartmentListResult> response = contactService.getDepartments().list()
                .setParentDepartmentId(deptId)
                .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)
                .execute();
        if(response.getCode() == 0 && null != response.getData().getItems())
            //将获取的子部门dept_id存入list并返回
            List<String> list = Arrays.stream(response.getData().getItems())
                            .map(item -> item.getDepartmentId())
                            .collect(Collectors.toList());
            return list;
        else
            return new ArrayList<>();
        
    

2.3.2 递归删除部门

	/**
	* 删除部门号为deptId的 部门及其子部门
	*/
   public void deleteAllDept(String deptId) throws Exception 
        List<String> subIds = feishuService.selectSubDept(deptId);
        //如果当前部门存在子部门,则继续删除其子部门
        if(subIds.size()>0)
            for (String subId : subIds) 
                deleteAllDept(subId);
            
        
        //子部门删除后 删除当前部门
        delDept(deptId);
    

	/**
	*	删除飞书平台的某个部门
	*/
    public void delDept(String deptId) throws Exception 
        Response<EmptyData> response = contactService.getDepartments()
                .delete()
                .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)
                .setDepartmentId(deptId).execute();
        if(response.getCode() == 0)
        	//删除本地飞书信息
            System.out.println("删除成功部门:"+deptId+ response.getMsg());
        else
            System.out.println("删除失败部门号:"+deptId+response.getMsg());
        
    

3. 用户对接

3.1 用户的创建

	User user = new User();
	//设置user的必要参数(暂时不设置leder)
	Response<UserCreateResult> response = contactService.getUsers()
			.create(user)	
	        .setUserIdType(Constants.CUSTOM_USER_ID_TYPE)
	        .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)
	        .execute();
	if(response.getCode() == 0)
	    //将user信息存入本地
	    System.out.println("创建成功用户:"+user.getUserId());
	else
	    System.out.println("创建失败用户:"+user.getUserId()+response.getMsg());
	

3.2 用户的更新

	User user = new User();
	//设置user必要参数
	//并更新leader信息
	user.setLeaderUserId(employee.getDirectorCode());
    Response<UserUpdateResult> response = contactService.getUsers()
    		.update(user)
            .setUserIdType(Constants.CUSTOM_USER_ID_TYPE)
            .setDepartmentIdType(Constants.CUSTOM_DEPT_ID_TYPE)
            .setUserId(user.getUserId()).execute();
    if(response.getCode() == 0)
        //将更新后的user存入本地
        System.out.println("更新成功用户:"+user.getUserId());
    else
        System.out.println("更新失败用户:"+user.getUserId()+response.getMsg());
    

3.3 用户的删除

      Response<EmptyData> response = contactService.getUsers()
      		.delete(new UserDeleteReqBody())
               .setUserId(userId)
               .setUserIdType(Constants.CUSTOM_USER_ID_TYPE)
               .execute();
      if(response.getCode() == 0)
          //将本地的user信息删除
          System.out.println("删除成功用户号:"+userId);
      else
          System.out.println("删除失败用户号:"+userId+response.getMsg());
      

5. 总结

5.1飞书组织架构同步顺序

  1. 创建新部门
    1. 创建user必须传入deptIds参数,所以必须先创建部门
    2. 创建部门必须按照,先创建父部门,再创建子部门(按层级顺序创建)
  2. 创建新用户 (暂时不设置leader信息,因为leader可能还未被创建)
  3. 更新部门、用户(部门leader设置,用户leader设置)
  4. 删除用户(删除部门必须确保部门中没有用户,所以先删除用户,再删除部门)
  5. 删除部门(递归删除当前部门及其子部门)

5.2 飞书通讯录API的使用总结

  1. 获取contactService

  2. 确定具体要操作的对象(用户、部门、用户组、人员类型等)和 某种操作
    具体的API格式大致为:

    	xxxService.getXXXXs.()
    		.todo()
    		.setParam()
    		.execute();
    

    例如:

    	contactService
    		.getDepartments()
    		.create(department)
    		.setDepartmentIdType(department_id")
    		.execute();
    
  3. 创建部门和删除部门时,需要确认部门的层级关系(是否有父部门或子部门)

  4. 设置部门leader或用户leader时,需保证leader用户已经被创建

飞书 + Lua 实现企业级组织架构登录认证

飞书是字节跳动旗下一款企业级协同办公软件,本文将介绍如何基于飞书开放平台的身份验证能力,使用 Lua 实现企业级组织架构的登录认证网关。

登录流程

让我们首先看一下飞书第三方网站免登的整体流程:

第一步: 网页后端发现用户未登录,请求身份验证;
第二步: 用户登录后,开放平台生成登录预授权码,302跳转至重定向地址;
第三步: 网页后端调用获取登录用户身份校验登录预授权码合法性,获取到用户身份;
第四步: 如需其他用户信息,网页后端可调用获取用户信息(身份验证)。

Lua 实现

飞书接口部分实现

获取应用的 access_token

function _M:get_app_access_token()
    local url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
    local body = {
        app_id = self.app_id,
        app_secret = self.app_secret
    }
    local res, err = http_post(url, body, nil)
    if not res then
        return nil, err
    end
    if res.status ~= 200 then
        return nil, res.body
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["tenant_access_token"]
end

通过回调 code 获取登录用户信息

function _M:get_login_user(code)
    local app_access_token, err = self:get_app_access_token()
    if not app_access_token then
        return nil, "get app_access_token failed: " .. err
    end
    local url = "https://open.feishu.cn/open-apis/authen/v1/access_token"
    local headers = {
        Authorization = "Bearer " .. app_access_token
    }
    local body = {
        grant_type = "authorization_code",
        code = code
    }
    ngx.log(ngx.ERR, json.encode(body))
    local res, err = http_post(url, body, headers)
    if not res then
        return nil, err
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["data"]
end

获取用户详细信息

获取登录用户信息时无法获取到用户的部门信息,故这里需要使用登录用户信息中的 open_id 获取用户的详细信息,同时 user_access_token 也是来自于获取到的登录用户信息。

function _M:get_user(user_access_token, open_id)
    local url = "https://open.feishu.cn/open-apis/contact/v3/users/" .. open_id
    local headers = {
        Authorization = "Bearer " .. user_access_token
    }
    local res, err = http_get(url, nil, headers)
    if not res then
        return nil, err
    end
    local data = json.decode(res.body)
    if data["code"] ~= 0 then
        return nil, res.body
    end
    return data["data"]["user"], nil
end

登录信息

JWT 登录凭证

我们使用 JWT 作为登录凭证,同时用于保存用户的 open_iddepartment_ids

-- 生成 token
function _M:sign_token(user)
    local open_id = user["open_id"]
    if not open_id or open_id == "" then
        return nil, "invalid open_id"
    end
    local department_ids = user["department_ids"]
    if not department_ids or type(department_ids) ~= "table" then
        return nil, "invalid department_ids"
    end

    return jwt:sign(
        self.jwt_secret,
        {
            header = {
                typ = "JWT",
                alg = jwt_header_alg,
                exp = ngx.time() + self.jwt_expire
            },
            payload = {
                open_id = open_id,
                department_ids = json.encode(department_ids)
            }
        }
    )
end

-- 验证与解析 token
function _M:verify_token()
    local token = ngx.var.cookie_feishu_auth_token
    if not token then
        return nil, "token not found"
    end

    local result = jwt:verify(self.jwt_secret, token)
    ngx.log(ngx.ERR, "jwt_obj: ", json.encode(result))
    if result["valid"] then
        local payload = result["payload"]
        if payload["department_ids"] and payload["open_id"] then
            return payload
        end
        return nil, "invalid token: " .. json.encode(result)
    end
    return nil, "invalid token: " .. json.encode(result)
end

使用 Cookie 存储登录凭证

ngx.header["Set-Cookie"] = self.cookie_key .. "=" .. token

组织架构白名单

我们在用户登录时获取用户的部门信息,或者在用户后续访问应用时解析登录凭证中的部门信息,根据设置的部门白名单,判断用户是否拥有访问应用的权限。

-- 部门白名单配置
_M.department_whitelist = {}

function _M:check_user_access(user)
    if type(self.department_whitelist) ~= "table" then
        ngx.log(ngx.ERR, "department_whitelist is not a table")
        return false
    end
    if #self.department_whitelist == 0 then
        return true
    end

    local department_ids = user["department_ids"]
    if not department_ids or department_ids == "" then
        return false
    end
    if type(department_ids) ~= "table" then
        department_ids = json.decode(department_ids)
    end
    for i=1, #department_ids do
        if has_value(self.department_whitelist, department_ids[i]) then
            return true
        end
    end
    return false
end

更多网关配置

同时支持 IP 黑名单和路由白名单配置。

-- IP 黑名单配置
_M.ip_blacklist = {}
-- 路由白名单配置
_M.uri_whitelist = {}

function _M:auth()
    local request_uri = ngx.var.uri
    ngx.log(ngx.ERR, "request uri: ", request_uri)

    if has_value(self.uri_whitelist, request_uri) then
        ngx.log(ngx.ERR, "uri in whitelist: ", request_uri)
        return
    end

    local request_ip = ngx.var.remote_addr
    if has_value(self.ip_blacklist, request_ip) then
        ngx.log(ngx.ERR, "forbided ip: ", request_ip)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    if request_uri == self.logout_uri then
        return self:logout()
    end

    local payload, err = self:verify_token()
    if payload then
        if self:check_user_access(payload) then
            return
        end

        ngx.log(ngx.ERR, "user access not permitted")
        self:clear_token()
        return self:sso()
    end
    ngx.log(ngx.ERR, "verify token failed: ", err)

    if request_uri ~= self.callback_uri then
        return self:sso()
    end
    return self:sso_callback()
end

使用

本文就不赘述 OpenResty 的安装了,可以参考我的另一篇文章《在 Ubuntu 上使用源码安装 OpenResty》

下载

cd /path/to
git clone git@github.com:ledgetech/lua-resty-http.git
git clone git@github.com:SkyLothar/lua-resty-jwt.git
git clone git@github.com:k8scat/lua-resty-feishu-auth.git

配置

lua_package_path "/path/to/lua-resty-feishu-auth/lib/?.lua;/path/to/lua-resty-jwt/lib/?.lua;/path/to/lua-resty-http/lib/?.lua;/path/to/lua-resty-redis/lib/?.lua;/path/to/lua-resty-redis-lock/lib/?.lua;;";

server {
    access_by_lua_block {
        local feishu_auth = require "resty.feishu_auth"
        feishu_auth.app_id = ""
        feishu_auth.app_secret = ""
        feishu_auth.callback_uri = "/feishu_auth_callback"
        feishu_auth.logout_uri = "/feishu_auth_logout"
        feishu_auth.app_domain = "feishu-auth.example.com"

        feishu_auth.jwt_secret = "thisisjwtsecret"

        feishu_auth.ip_blacklist = {"47.1.2.3"}
        feishu_auth.uri_whitelist = {"/"}
        feishu_auth.department_whitelist = {"0"}

        feishu_auth:auth()
    }
}

配置说明

  • app_id 用于设置飞书企业自建应用的 App ID
  • app_secret 用于设置飞书企业自建应用的 App Secret
  • callback_uri 用于设置飞书网页登录后的回调地址(需在飞书企业自建应用的安全设置中设置重定向 URL)
  • logout_uri 用于设置登出地址
  • app_domain 用于设置访问域名(需和业务服务的访问域名一致)
  • jwt_secret 用于设置 JWT secret
  • ip_blacklist 用于设置 IP 黑名单
  • uri_whitelist 用于设置地址白名单,例如首页不需要登录认证
  • department_whitelist 用于设置部门白名单(字符串)

应用权限说明

  • 获取部门基础信息
  • 获取部门组织架构信息
  • 以应用身份读取通讯录
  • 获取用户组织架构信息
  • 获取用户基本信息

开源

本项目已完成且已在 GitHub 上开源:k8scat/lua-resty-feishu-auth,希望大家可以动动手指点个 Star,表示对本项目的肯定与支持!

以上是关于飞书组织架构同步(部门和用户) Java-API 的使用的主要内容,如果未能解决你的问题,请参考以下文章

从美团大象和字节飞书看数字组织的架构设计

985统计学硕士的腾讯 飞书 抖音 Tiktok 数据分析面经(均已拿offer)

H3 BPM V10.0 产品更新日志

Windows Server2008建立组织单位组用户以及文件赋权部门

钉钉,企业微信,华为welink,头条飞书,各有啥优势?

在毫秒量级上做到“更快”!DataTester 助力飞书提升页面秒开率