前言
有了上一篇釘釘企業內部應用SSO單點登錄實戰及踩坑過程之後,再去看釘釘的文檔和接口就顯得輕車熟路了。
明確需求:定時把釘釘的用户同步到企業自己開發的系統中,以便讓企業內的員工都能使用SSO單點登錄。
確定接口
借鑑上一篇的經驗,我們優先選擇V2版本的接口。
右側提供了返回值的實體,注意到包括userId、姓名、電話這三個關鍵字段存在,説明該接口滿足需求。
把鼠標放到token上面,可以看到該token來源於:
這樣,兩個接口就確定了。
前置設置
參考上一篇文章,需要以下信息:
- 我們需要記下cropId、ClientId、Client Secret
- 後端會定時獲取access_token
- 每次登錄會實時生成一次性code
- 其他信息都不再需要了
開通以下權限(圖中的權限都需要):
特別注意獲取手機號權限,如無此權限,接口仍可以正常返回信息,但不包含手機號。
安全設置中添加ip白名單:
同步功能只需要後端,並且釘釘接口的部分並不算難,主要解決一些細節問題。
定時器
同步通常由定時器進行觸發,例如這樣:
@Component
public class DingdingUserSyncSchedule {
@Scheduled(cron = "0 10 02 * * *")
public void sync() {
System.out.println("同步定時器被觸發");
}
}
其中,@Component使得此類作為組件被IoC加載,@Scheduled使得此方法會定時執行。
@Scheduled括號中的內容是cron表達式:
{秒} {分} {時} {日} {月} {星期} {年份(可選)}
其中, * 表示通配符,即該位置無論是任何值都會執行。
所以測試時可以使用@Scheduled(cron = "0 * * * * *")表示每分鐘0秒觸發一次,生產環境可以使用@Scheduled(cron = "0 10 02 * * *")表示每天凌晨2:10觸發一次。
啓動後就可以看到打印日誌了:
釘釘接口遞歸
由於是分頁獲取,一次只能獲取一部分,所以要遞歸調用接口,拼接list,這樣系統內的方法就能一次返回所有用户。
public List<OapiV2UserListResponse.ListUserResponse> getDingdingUserList(Long deptId, Long cursor) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/list");
OapiV2UserListRequest req = new OapiV2UserListRequest();
req.setDeptId(deptId);
req.setCursor(cursor);
req.setSize(10L);
req.setOrderField("modify_desc");
req.setContainAccessLimit(false);
req.setLanguage("zh_CN");
OapiV2UserListResponse rsp = client.execute(req, getAccessToken());
System.out.println(rsp.getResult().getHasMore());
System.out.println(rsp.getResult().getList());
if (!rsp.isSuccess()) {
this.logger.error("Failed to get user list: {}", rsp.getErrmsg());
return null;
}
List<OapiV2UserListResponse.ListUserResponse> list = rsp.getResult().getList();
// 如果還有,就遞歸
if (rsp.getResult().getHasMore()) {
list.addAll(getDingdingUserList(deptId, rsp.getResult().getNextCursor()));
return list;
}
// 沒有了就返回list
return list;
} catch (ApiException e) {
e.printStackTrace();
}
return null;
}
這樣一次拿到了所有用户。
此外,還有accesstoken的獲取,見上一篇文章,一模一樣。
此處貼上代碼:
@Value("${app.dingDing.cropId}")
private String corpId;
@Value("${app.dingDing.ClientId}")
private String clientId;
@Value("${app.dingDing.ClientSecret}")
private String clientSecret;
// 緩存的 token 值
private String cachedToken;
// 過期時間戳(毫秒)
private long expireAt = 0;
/**
* getAccessToken
*/
public String getAccessToken() {
// 判斷緩存是否還有效(預留200秒作為刷新緩衝)
if (cachedToken != null && (expireAt - 200_000) > System.currentTimeMillis()) {
this.logger.info("accessToken: {}", cachedToken);
return cachedToken;
}
try {
// 獲取client、構造請求
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest req = new OapiGettokenRequest();
req.setAppkey(clientId);
req.setAppsecret(clientSecret);
req.setHttpMethod("GET");
// 發送請求獲取access_token
String accessToken = client.execute(req).getAccessToken();
this.logger.info("accessToken: {}", accessToken);
// 緩存token,設置有效期7200秒
this.cachedToken = accessToken;
this.expireAt = System.currentTimeMillis() + 7200_000;
} catch (ApiException err) {
this.logger.error("獲取accessToken失敗:{}", err.getErrMsg());
err.printStackTrace();
}
return cachedToken;
}
同步用户
這步就是把釘釘用户同步到系統中,這沒什麼難的,由於不同系統的表結構不同,僅供參考:
/**
* 根據返回值更新用户信息
* @param list
*/
public void updateDingdingUserList(List<OapiV2UserListResponse.ListUserResponse> list) {
for (OapiV2UserListResponse.ListUserResponse userItem : list) {
this.logger.info("開始更新釘釘用户: {}{}", userItem.getUserid(), userItem.getName());
// 查當前系統的釘釘用户
DingdingUser dingdingUser = dingdingUserRepository.findByUserId(userItem.getUserid()).orElse(null);
// 如果查不到,就新建
if (dingdingUser == null) {
this.logger.info("釘釘用户{}{}不存在,新建", userItem.getUserid(), userItem.getName());
dingdingUser = new DingdingUser();
dingdingUser.setUserId(userItem.getUserid());
dingdingUser.setName(userItem.getName());
dingdingUserRepository.save(dingdingUser);
}
// 查系統用户
User user = userRepository.findByDingdingUser(dingdingUser).orElse(null);
if (user == null) {
this.logger.info("系統用户{}{}不存在,新建", userItem.getUserid(), userItem.getName());
// 查不到,就新建
user = new User();
user.setDingdingUser(dingdingUser);
user.setName(userItem.getName());
user.setPhone(userItem.getMobile());
user.setPassword(this.systemConfigService.getByDefaultPassword().getValue());
userRepository.save(user);
} else {
this.logger.info("系統用户{}{}存在,id為{}", userItem.getUserid(), userItem.getName(), user.getId());
// 查到了,分別判斷關聯的用户、姓名、電話,為空則更新,否則不動
if (user.getDingdingUser() == null) {
user.setDingdingUser(dingdingUser);
}
if (user.getName() == null) {
user.setName(userItem.getName());
}
if (user.getPhone() == null) {
user.setPhone(userItem.getMobile());
}
userRepository.save(user);
}
this.logger.info("釘釘用户: {}{}同步完成", userItem.getUserid(), userItem.getName());
}
}
關鍵在於——打日誌,如果效果和預期不符時,方便調試。
在釘釘的Service寫一個同步入口方法
同步入口方法,後續如果需要邏輯變更時更靈活。
@Override
public void syncDingdingUser() {
// 加一個開關,默認不啓用同步,避免頻繁請求釘釘超過API調用次數限制
if (!Objects.equals(this.enableSync, "false")) {
return;
}
Long deptId = 1L;
Long cursor = 0L;
// 調用用户列表接口
List<OapiV2UserListResponse.ListUserResponse> listUserResponse = getDingdingUserList(deptId, cursor);
// 調用系統內方法更新信息
updateDingdingUserList(listUserResponse);
}
定時器觸發時調用入口方法
@Component
public class DingdingUserSyncSchedule {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final DingTalkService dingTalkService;
DingdingUserSyncSchedule(DingTalkService dingTalkService) {
this.dingTalkService = dingTalkService;
}
@Scheduled(cron = "0 * * * * *")
public void sync() {
this.logger.info("DingdingUserSyncSchedule 開始執行釘釘用户同步");
this.dingTalkService.syncDingdingUser();
this.logger.info("DingdingUserSyncSchedule 釘釘用户同步結束");
}
}
啓動,觀察日誌
大量同步日誌打出,且沒有報錯,説明成功了。
後記
當前系統的業務沒有細分部門,如果需要考慮部門的話,應該增加以下接口:
開通以下權限:
然後從根部門的id = 1逐級遞歸,就能獲取所有部門,遍歷部門來分別獲取用户,最後同步即可。