项目由来
这个项目是我本科毕设,为即将读研的实验室开发一个绩效管理系统,当时钉钉相关的资料比较少,文档还不是十分完善,开发时,还是遇到了不少问题。后来疫情发生了,钉钉就突然火了🤣
项目地址
后端:dingtalk-springboot
前端:dingtalk-vue
钉钉鉴权
这些资料网上应该有不少了,列出我的代码
前端鉴权
- 前端鉴权源码:dingtalk.js
- import dingtalk-jsapi 部分,是按需导入, 为了减小依赖包的大小
- import authenticate 用于从后端获取签名信息 :
import {
authenticate
} from "@/api/common"; // jsapi 鉴权时获取签名信息
import * as dd from "dingtalk-jsapi/entry/union"; // 按需应用,微应用部分
import requestAuthCode from "dingtalk-jsapi/api/runtime/permission/requestAuthCode"; // 登陆用临时授权码
import choose from "dingtalk-jsapi/api/biz/contact/choose"; // PC 通讯录选人
/**
* 鉴权
* @param url 前端当前的url
*/
function ddconfig(url) {
return authenticate(url).then(res => {
dd.config({
agentId: res.data.agentId, // 必填,微应用ID
corpId: res.data.corpId, //必填,企业ID
timeStamp: res.data.timeStamp, // 必填,生成签名的时间戳
nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
signature: res.data.signature, // 必填,签名
type: 0,
jsApiList: [
"runtime.info",
"biz.contact.choose",
"device.notification.confirm",
"device.notification.alert",
"device.notification.prompt",
"biz.ding.post",
"biz.util.openLink"
] // 必填,需要使用的jsapi列表,注意:不要带dd。
});
});
}
// 通讯录选人
export function contactChoose(url, userids) {
return new Promise((resolve, reject) => {
ddconfig(url)
.then(() => {
choose({
users: userids,
multiple: true, //是否多选:true多选 false单选; 默认true
corpId: process.env.VUE_APP_CORPID, //企业id
max: 10 //人数限制,当multiple为true才生效,可选范围1-1500
}).then(res => {
res = JSON.parse(JSON.stringify(res).replace(/emplId/g, "userid"));
resolve(res);
});
})
.catch(err => {
reject(err);
});
});
}
// 获取登陆用临时授权码
export function getAuthCode(corpId) {
return requestAuthCode({
corpId: corpId
});
}
后端鉴权
- 后端sdk使用源码:DingTalkUtils.java
package com.softeng.dingtalk.component;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.*;
import com.dingtalk.api.response.*;
import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author zhanyeye
* @description DingTalk 服务端API 工具组件
* @date 11/13/2019
*/
@Slf4j
@Component
public class DingTalkUtils {
private static String CORPID;
private static String APP_KEY;
private static String APP_SECRET;
private static String CHAT_ID;
private static String AGENTID;
private static String DOMAIN;
@Value("${my.corpid}")
public void setCORPID(String corpid) {
CORPID = corpid;
}
@Value("${my.app_key}")
public void setAppKey(String appKey) {
APP_KEY = appKey;
}
@Value("${my.app_secret}")
public void setAppSecret(String appSecret) {
APP_SECRET = appSecret;
}
@Value("${my.chat_id}")
public void setChatId(String chatId) {
CHAT_ID = chatId;
}
@Value("${my.agent_id}")
public void setAGENTID(String agentid) {
AGENTID = agentid;
}
@Value("${my.domain}")
public void setDOMAIN(String domain) {
DOMAIN = domain;
}
/**
* 缓存时间 1小时 50分钟
*/
private static final long cacheTime = 1000 * 60 * 55 * 2;
/**
* 缓存的accessToken: 不可直接调用,以防过期
*/
private static String accessToken;
/**
* 缓存时间
*/
private static long tokenTime;
/**
* 缓存的accessToken jsapi_ticket: 不可直接调用,以防过期
*/
private static String jsapiTicket;
/**
* 缓存时间
*/
private static long ticketTime;
/**
* 获取 AccessToken
* @return java.lang.String
* @Date 9:10 PM 11/13/2019
**/
public String getAccessToken() {
long curTime = System.currentTimeMillis();
if (accessToken == null || curTime - tokenTime >= cacheTime ) {
DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest request = new OapiGettokenRequest();
request.setAppkey(APP_KEY);
request.setAppsecret(APP_SECRET);
request.setHttpMethod("GET");
try {
OapiGettokenResponse response = client.execute(request);
accessToken = response.getAccessToken();
tokenTime = System.currentTimeMillis();
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "DingtalkUtils 获取accesstoken失败");
}
log.debug("AccessToken 快要过期,重新获取");
}
return accessToken;
}
/**
* 获取 Jsapi Ticket
* @return java.lang.String
* @Date 8:20 AM 2/23/2020
**/
public String getJsapiTicket() {
long curTime = System.currentTimeMillis();
if (jsapiTicket == null || curTime - ticketTime >= cacheTime) {
DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/get_jsapi_ticket");
OapiGetJsapiTicketRequest req = new OapiGetJsapiTicketRequest();
req.setTopHttpMethod("GET");
try {
OapiGetJsapiTicketResponse response = client.execute(req, getAccessToken());
jsapiTicket = response.getTicket();
ticketTime = System.currentTimeMillis();
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "DingtalkUtils 获取JsapiTicket失败");
}
log.debug("JsapiTicket 快要过期,重新获取");
}
return jsapiTicket;
}
/**
* 获得userid : 通过 access_token 和 requestAuthcode;在内部调用了getAccessToken(),不用传参
* @param requestAuthCode
* @return java.lang.String
* @Date 5:07 PM 1/13/2020
**/
public String getUserId(String requestAuthCode) {
String userId = null;
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/getuserinfo");
OapiUserGetuserinfoRequest request = new OapiUserGetuserinfoRequest();
request.setCode(requestAuthCode);
request.setHttpMethod("GET");
try {
OapiUserGetuserinfoResponse response = client.execute(request, getAccessToken());
userId = response.getUserid();
} catch (ApiException e) {
e.printStackTrace();
}
return userId;
}
/**
* 根据 userid 获取用户详细信息
* @param userid
* @return
*/
public OapiUserGetResponse fetchUserDetail(String userid) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/get");
OapiUserGetRequest request = new OapiUserGetRequest();
request.setUserid(userid);
request.setHttpMethod("GET");
OapiUserGetResponse response;
try {
response = client.execute(request, getAccessToken());
} catch (ApiException e) {
log.error("getUserDetail fail", e);
throw new RuntimeException();
}
return response;
}
/**
* 获取周报信息
* @param userid
* @param date
* @return
*/
public Map getReport(String userid, LocalDate date) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/report/list");
OapiReportListRequest request = new OapiReportListRequest();
request.setUserid(userid);
Long startTime = LocalDateTime.of(date, LocalTime.of(8,0)).toInstant(ZoneOffset.of("+8")).toEpochMilli();
//开始时间
request.setStartTime(startTime);
//结束时间
request.setEndTime(startTime + TimeUnit.DAYS.toMillis(5));
request.setCursor(0L);
request.setSize(1L);
OapiReportListResponse response;
try {
response = client.execute(request, getAccessToken());
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取周报失败");
}
if (response.getResult().getDataList().size() == 0) {
// 无数据
return Map.of();
} else {
List<OapiReportListResponse.JsonObject> contents = response.getResult().getDataList().get(0).getContents().stream()
.filter((item) -> !item.getValue().isEmpty())
.collect(Collectors.toList());
return Map.of("contents", contents);
}
}
/**
* 获取部门id
* @return
*/
public List<String> listDepid() {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/department/list");
OapiDepartmentListRequest request = new OapiDepartmentListRequest();
request.setHttpMethod("GET");
OapiDepartmentListResponse response;
try {
response = client.execute(request, getAccessToken());
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取部门Id 失败");
}
return response.getDepartment().stream().map(x -> String.valueOf(x.getId())).collect(Collectors.toList());
}
/**
* 查询所有部门信息
* @return
*/
public List<OapiDepartmentListResponse.Department> fetchDeptInfo() {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/department/list");
OapiDepartmentListRequest request = new OapiDepartmentListRequest();
request.setHttpMethod("GET");
OapiDepartmentListResponse response;
try {
response = client.execute(request, getAccessToken());
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取部门Id 失败");
}
return response.getDepartment();
}
/**
* 获取整个部门的userid
* @param depid
* @return
*/
public List<String> listUserId(String depid) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/getDeptMember");
OapiUserGetDeptMemberRequest req = new OapiUserGetDeptMemberRequest();
req.setDeptId(depid);
req.setHttpMethod("GET");
OapiUserGetDeptMemberResponse response;
try {
response = client.execute(req, getAccessToken());
} catch (ApiException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取getUserIds失败");
}
return response.getUserIds();
}
// 生成钉钉内部跳转链接
/**
* 生成跳转到投票页面的钉钉链接
* @param isInternal 是否是内部论文评审投票
* @param pid 对应的论文id
* @return
*/
private String createDingTalkLink(boolean isExternal, int pid) {
StringBuffer curl = null;
if (isExternal) {
// 外部论文评审的链接
curl = new StringBuffer().append("dingtalk://dingtalkclient/action/openapp?corpid=").append(CORPID)
.append("&container_type=work_platform&app_id=0_").append(AGENTID).append("&redirect_type=jump&redirect_url=")
.append(DOMAIN).append("/paper/ex-detail/").append(pid).append("/vote");
} else {
// 内部论文评审的链接
curl = new StringBuffer().append("dingtalk://dingtalkclient/action/openapp?corpid=").append(CORPID)
.append("&container_type=work_platform&app_id=0_").append(AGENTID).append("&redirect_type=jump&redirect_url=")
.append(DOMAIN).append("/paper/in-detail/").append(pid).append("/vote");
}
log.debug(curl.toString());
return curl.toString();
}
/**
* 发起投票时向群中发送消息
* @param pid
* @param title
* @param endtime
* @param namelist
*/
public void sendVoteMsg(int pid, boolean isExternal, String title, String endtime, List<String> namelist) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/send");
OapiChatSendRequest request = new OapiChatSendRequest();
request.setChatid(CHAT_ID);
OapiChatSendRequest.ActionCard actionCard = new OapiChatSendRequest.ActionCard();
if (!isExternal) {
// 如果是内部评审投票
StringBuffer content = new StringBuffer().append(" #### 投票 \n ##### 论文: ").append(title).append(" \n ##### 作者: ");
for (String name : namelist) {
content.append(name).append(", ");
}
content.append(" \n 截止时间: ").append(endtime);
actionCard.setTitle("内部评审投票");
actionCard.setMarkdown(content.toString());
} else {
// 如果是外部评审投票
StringBuffer content = new StringBuffer().append(" #### 投票 \n ##### 论文: ").append(title);
content.append(" \n 截止时间: ").append(endtime);
actionCard.setTitle("外部评审投票");
actionCard.setMarkdown(content.toString());
}
actionCard.setSingleTitle("前往投票");
actionCard.setSingleUrl(createDingTalkLink(isExternal, pid));
request.setActionCard(actionCard);
request.setMsgtype("action_card");
try {
OapiChatSendResponse response = client.execute(request, getAccessToken());
} catch (ApiException e) {
e.printStackTrace();
}
}
/**
* 发送投票结果
* @param pid
* @param title
* @param result
* @param accept
* @param total
*/
public void sendVoteResult(int pid, String title, boolean result, int accept, int total, boolean isExternal) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/send");
OapiChatSendRequest request = new OapiChatSendRequest();
request.setChatid(CHAT_ID);
OapiChatSendRequest.ActionCard actionCard = new OapiChatSendRequest.ActionCard();
StringBuffer content = new StringBuffer().append(" #### 投票结果 \n ##### 论文: ").append(title)
.append(" \n 最终结果: ").append(result ? "Accept" : "reject")
.append(" \n Accept: ").append(accept).append(" 票 \n ")
.append("Reject: ").append(total-accept).append(" 票 \n ")
.append("已参与人数: ").append(total).append("人 \n ");
actionCard.setTitle("投票结果");
actionCard.setMarkdown(content.toString());
actionCard.setSingleTitle("查看详情");
actionCard.setSingleUrl(createDingTalkLink(isExternal, pid));
request.setActionCard(actionCard);
request.setMsgtype("action_card");
try {
OapiChatSendResponse response = client.execute(request, getAccessToken());
log.debug(response.getBody());
} catch (ApiException e) {
e.printStackTrace();
}
}
/**
* 字节数组转化成十六进制字符串
* @param hash
* @return
*/
private String bytesToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
/**
* 计算鉴权 signature
* @param ticket
* @param nonceStr
* @param timeStamp
* @param url
* @return
*/
private String sign(String ticket, String nonceStr, long timeStamp, String url) {
String plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + String.valueOf(timeStamp)
+ "&url=" + url;
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.reset();
sha1.update(plain.getBytes("UTF-8"));
return bytesToHex(sha1.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 返回鉴权结果
* @param url
* @return
*/
public Map authentication(String url) {
long timeStamp = System.currentTimeMillis();
String nonceStr = "todowhatliesclearathand";
String signature = sign(getJsapiTicket(),nonceStr, timeStamp, url);
return Map.of("agentId", AGENTID,"url", url, "nonceStr", nonceStr, "timeStamp", timeStamp, "corpId", CORPID, "signature", signature);
}
}