前期教程在钉钉中都有详细的教程(地址:open.dingtalk.com/document/or…) 这里只描述需要自己手动操作的。
一、创建springboot/spring项目
本项目使用的是Springboot。
1.添加依赖
其中lib中的两个jar包可以在钉钉给的示例文档中找到。
git clone https://github.com/open-dingtalk/h5app-corp-quickstart.git
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/taobao-sdk-java-auto_1479188381469-20210207.jar</systemPath>
</dependency>
<dependency>
<groupId>com.taobao.top</groupId>
<artifactId>lippi-oapi-encrpt</artifactId>
<version>dingtalk-SNAPSHOT</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/lippi-oapi-encrpt.jar</systemPath>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
2.编写配置文件
dingtalk:
app_key: 你的app_key
app_secret: 你的app_secret
agent_id: 你的agent_id
corp_id: 你公司的组织id
其他配置文件请自由添加
3.创建实体及常量类
3.1创建配置实体类
/**
* 应用凭证配置
*/
@Configuration
@Data
public class AppConfig {
@Value("${dingtalk.app_key}")
private String appKey;
@Value("${dingtalk.app_secret}")
private String appSecret;
@Value("${dingtalk.agent_id}")
private String agentId;
@Value("${dingtalk.corp_id}")
private String corpId;
}
3.2项目中的常量定义类
public class Constant {
/**
* 数据加密密钥。用于回调数据的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成
*/
public static final String ENCODING_AES_KEY = "你的aes_key";
/**
* 加解密需要用到的token,企业可以随机填写。如 "12345"
*/
public static final String TOKEN = "你的token"
3.3钉钉开放接口网关常量
public class UrlConstant {
private static final String HOST = "https://oapi.dingtalk.com";
/**
* 获取access_token url
*/
public static final String URL_GET_TOKEN = HOST + "/gettoken";
/**
* 获取jsapi_ticket url
*/
public static final String URL_GET_JSTICKET = HOST + "/get_jsapi_ticket";
/**
* 通过免登授权码获取用户信息 url
*/
public static final String URL_GET_USER_INFO = HOST + "/user/getuserinfo";
/**
* 根据用户id获取用户详情 url
*/
public static final String URL_USER_GET = HOST + "/user/get";
/**
* 获取审批实例的接口url
*/
public static final String URL_PROCESSINSTANCE_GET =HOST + "/topapi/processinstance/get";
3.4创建User实体类
/**
* 用户详情
*/
@Data
public class User implements Serializable {
/**
* 用户userId
*/
private String userid;
/**
* 用户名称
*/
private String name;
/**
* 头像URL
*/
private String avatar;
/**
* 用户部门
*
*/
private List department;
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
3.5创建token实体类
此实体可以没有,看实际情况,这里创建这个实体的目的是将从钉钉请求回来的token存储到的数据库中,然后通过计算token的时间与设定的时间差,来判读token是否过期。
@Data
@SuppressWarnings("serial")
public class Token extends Model<Token> implements Serializable{
private String appkey;
private String type;
private String code;
@TableField(fill = FieldFill.INSERT)
private Long beginTime;
}
3.6统一接口返回实体
/**
* service层返回对象列表封装
*/
public class ServiceResult<T> implements Serializable {
private boolean success = false;
private String code;
private String message;
private T result;
private ServiceResult() {
}
public static <T> ServiceResult<T> success(T result) {
ServiceResult<T> item = new ServiceResult<T>();
item.success = true;
item.result = result;
item.code = "200";
item.message = "success";
return item;
}
public static <T> ServiceResult<T> failure(String errorCode, String errorMessage) {
ServiceResult<T> item = new ServiceResult<T>();
item.success = false;
item.code = errorCode;
item.message = errorMessage;
return item;
}
public static <T> ServiceResult<T> failure(String errorCode) {
ServiceResult<T> item = new ServiceResult<T>();
item.success = false;
item.code = errorCode;
item.message = "failure";
return item;
}
public boolean hasResult() {
return result != null;
}
public boolean isSuccess() {
return success;
}
public T getResult() {
return result;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
4.钉钉开放平台加解密异常类
public class DingtalkEncryptException extends Exception {
/**
* 成功
*/
public static final int SUCCESS = 0;
/**
* 加密明文文本非法
*/
public final static int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
/**
* 加密时间戳参数非法
*/
public final static int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
/**
* 加密随机字符串参数非法
*/
public final static int ENCRYPTION_NONCE_ILLEGAL = 900003;
/**
* 不合法的aeskey
*/
public final static int AES_KEY_ILLEGAL = 900004;
/**
* 签名不匹配
*/
public final static int SIGNATURE_NOT_MATCH = 900005;
/**
* 计算签名错误
*/
public final static int COMPUTE_SIGNATURE_ERROR = 900006;
/**
* 计算加密文字错误
*/
public final static int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
/**
* 计算解密文字错误
*/
public final static int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
/**
* 计算解密文字长度不匹配
*/
public final static int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
/**
* 计算解密文字corpid不匹配
*/
public final static int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
private static Map<Integer, String> msgMap = new HashMap<>();
static {
msgMap.put(SUCCESS, "成功");
msgMap.put(ENCRYPTION_PLAINTEXT_ILLEGAL, "加密明文文本非法");
msgMap.put(ENCRYPTION_TIMESTAMP_ILLEGAL, "加密时间戳参数非法");
msgMap.put(ENCRYPTION_NONCE_ILLEGAL, "加密随机字符串参数非法");
msgMap.put(SIGNATURE_NOT_MATCH, "签名不匹配");
msgMap.put(COMPUTE_SIGNATURE_ERROR, "签名计算失败");
msgMap.put(AES_KEY_ILLEGAL, "不合法的aes key");
msgMap.put(COMPUTE_ENCRYPT_TEXT_ERROR, "计算加密文字错误");
msgMap.put(COMPUTE_DECRYPT_TEXT_ERROR, "计算解密文字错误");
msgMap.put(COMPUTE_DECRYPT_TEXT_LENGTH_ERROR, "计算解密文字长度不匹配");
msgMap.put(COMPUTE_DECRYPT_TEXT_CORPID_ERROR, "计算解密文字corpid或者suiteKey不匹配");
}
private Integer code;
public DingtalkEncryptException(Integer exceptionCode) {
this(exceptionCode, null);
}
public DingtalkEncryptException(Integer exceptionCode, Throwable e) {
super(msgMap.get(exceptionCode), e);
this.code = exceptionCode;
}
public Integer getCode() {
return code;
}
5.service业务方法
5.1TokenServiceImpl(token的获取以及判断是否过期)
@Service
public class TKServiceImpl {
private static final Logger log = LoggerFactory.getLogger(TKService.class);
/**
* 缓存时间:一小时50分钟
*/
private static final long CACHE_TTL = 60 * 55 * 2 * 1000;
private AppConfig appConfig;
private TokenDao tokenDao;
@Autowired
public TKServiceImpl(AppConfig appConfig,TokenDao tokenDao) {
this.appConfig = appConfig;
this.tokenDao=tokenDao;
}
/**
* 在此方法中,为了避免频繁获取access_token,
* 在距离上一次获取access_token时间在两个小时之内的情况,
* 将直接从持久化存储中读取access_token
*
* 因为access_token和jsapi_ticket的过期时间都是7200秒
* 所以在获取access_token的同时也去获取了jsapi_ticket
* 注:jsapi_ticket是在前端页面JSAPI做权限验证配置的时候需要使用的
* 具体信息请查看开发者文档--权限验证配置
*
* @return accessToken 或错误信息
*/
public ServiceResult<String> getAccessToken() {
final String type="accessToken";
// 从redis中获取token
String accessToken = getFromCache(type);
System.out.println("accessToken:"+accessToken);
if (accessToken != null) {
return ServiceResult.success(accessToken);
}
DefaultDingTalkClient client = new DefaultDingTalkClient(UrlConstant.URL_GET_TOKEN);
OapiGettokenRequest request = new OapiGettokenRequest();
OapiGettokenResponse response;
request.setAppkey(appConfig.getAppKey());
request.setAppsecret(appConfig.getAppSecret());
request.setHttpMethod("GET");
try {
response = client.execute(request);
} catch (ApiException e) {
log.error("getAccessToken failed", e);
return ServiceResult.failure(e.getErrCode(), e.getErrMsg());
}
accessToken = response.getAccessToken();
//放到数据库中
putToCache(accessToken);
return ServiceResult.success(accessToken);
}
/**
* 从数据库中获取token并检查是否已过期
*
* @param type 类型
* @return token值 或 null (过期或未查到)
*/
private String getFromCache(String type) {
QueryWrapper<Token> wrapper=new QueryWrapper<>();
wrapper.eq("appkey", appConfig.getAppKey());
wrapper.eq("type",type);
Token token = tokenDao.selectOne(wrapper);
if (token != null) {
if (System.currentTimeMillis() - token.getBeginTime() <= CACHE_TTL){
return token.getCode();
}else {
tokenDao.delete(wrapper);
}
}
return null;
}
/**
* 将token存储到数据库中
*
* @param accessToken token
*/
private void putToCache(String accessToken) {
Token token = new Token();
token.setAppkey(appConfig.getAppKey());
token.setCode(accessToken);
token.setType("accessToken");
tokenDao.insert(token);
}
}
5.2UserService
public interface UserService {
/**
* 访问/user/get 获取用户名称
*
* @param accessToken access_token
* @param userId 用户userId
* @return 用户名称或错误信息
*/
User getUser(String accessToken, String userId) throws ApiException;
}
@Service
public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
/**
* 访问/user/get 获取用户名称
*
* @param accessToken access_token
* @param userId 用户userId
* @return 用户名称或错误信息
*/
@Override
public User getUser(String accessToken, String userId) throws ApiException {
DingTalkClient client = new DefaultDingTalkClient(URL_USER_GET);
OapiUserGetRequest request = new OapiUserGetRequest();
request.setUserid(userId);
request.setHttpMethod("GET");
OapiUserGetResponse response;
response = client.execute(request, accessToken);
User user = new User();
user.setName(response.getName());
user.setUserid(response.getUserid());
user.setAvatar(response.getAvatar());
user.setDepartment(response.getDepartment());
return user;
}
}
6.Controller接口
6.1 AuthController
/**
* 身份验证(免登)接口
*/
@RestController
@CrossOrigin
@RequestMapping("user")
public class AuthController {
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
private TKService tkService;
private AppConfig appConfig;
private UserService userService;
@Autowired
public AuthController(
AppConfig appConfig,
TKService tkService,
UserService userService
) {
this.appConfig = appConfig;
this.tkService = tkService;
this.userService = userService;
}
/**
* 钉钉用户登录,显示当前登录用户的userId和名称
*
* @param authCode 免登临时authCode
* @return 当前用户的信息
*/
@PostMapping("/getUserInfo/{authCode}")
public ServiceResult<User> login(@PathVariable("authCode") String authCode) {
String accessToken;
// 获取accessToken
ServiceResult<String> accessTokenSr = tkService.getAccessToken();
if (!accessTokenSr.isSuccess()) {
return ServiceResult.failure(accessTokenSr.getCode(), accessTokenSr.getMessage());
}
accessToken = accessTokenSr.getResult();
System.out.println(accessToken);
// 获取用户userId
ServiceResult<String> userIdSr = getUserInfo(accessToken, authCode);
if (!userIdSr.isSuccess()) {
return ServiceResult.failure(userIdSr.getCode(), userIdSr.getMessage());
}
// 获取用户ID
User user;
try {
user = userService.getUser(accessToken, userIdSr.getResult());
} catch (ApiException e) {
log.error("Failed to {}", URL_USER_GET, e);
return ServiceResult.failure(e.getErrCode(), "Failed to getUserName: " + e.getErrMsg());
}
return ServiceResult.success(user);
}
/**
* 访问/user/getuserinfo接口获取用户userId
*
* @param accessToken access_token
* @param authCode 临时授权码
* @return 用户userId或错误信息
*/
private ServiceResult<String> getUserInfo(String accessToken, String authCode) {
DingTalkClient client = new DefaultDingTalkClient(URL_GET_USER_INFO);
OapiUserGetuserinfoRequest request = new OapiUserGetuserinfoRequest();
request.setCode(authCode);
request.setHttpMethod("GET");
OapiUserGetuserinfoResponse response;
try {
response = client.execute(request, accessToken);
} catch (ApiException e) {
log.error("Failed to {}", URL_GET_USER_INFO, e);
return ServiceResult.failure(e.getErrCode(), "Failed to getUserInfo: " + e.getErrMsg());
}
if (!response.isSuccess()) {
return ServiceResult.failure(response.getErrorCode(), response.getErrmsg());
}
return ServiceResult.success(response.getUserid());
}
}
二、前端(vue项目)
由于本人非专业前端。写法不规范,甚至可以说没有规范,只是项目能跑,能达到预期的效果而已。 本项目使用vue-admin-template进行修改。
1.添加钉钉的包并修改.env.development文件中的后端接口
npm install dingtalk-jsapi --save
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = 'http://127.0.0.1:8088'
2.删除permission.js文件中的router.beforeEach方法
在这里修改虽然可以做到我的预期效果,但是可能不符合规范。由于我不需要登录页面。所以就删除了。
router.beforeEach(async (to, from, next) => {
NProgress.start()
document.title = getPageTitle(to.meta.title)
NProgress.done()
next()
})
3.在store.modules.user.js文件中添加getUserInfo方法用于获取用户信息。
getUserInfo({ commit }, code) {
return new Promise((resolve, reject) => {
getUserInfo(code).then(response => {
commit('SET_NAME', response.result.name)
commit('SET_AVATAR', response.result.avatar)
commit('SET_USERID', response.result.userid)
commit('SET_DEPT', response.result.department)
resolve()
}).catch(error => {
reject(error)
})
})
},
4.创建common.dingding.js文件
import * as dd from 'dingtalk-jsapi'
export function getCode(callback) {
const corpId = 'ding3b0b26da79cdc8f235c2f4657eb6378f'
if (dd.env.platform !== 'notInDingTalk') {
dd.ready(() => {
// 使用SDK 获取免登授权码
dd.runtime.permission.requestAuthCode({
corpId: corpId,
onSuccess: (info) => {
// 根据钉钉提供的api 获得code后,再次调用这个callback方法
// 由于是钉钉获取code是异步操作,不知道什么时候执行完毕
// callback 函数会等他执行完毕后在自己调用自己
callback(info.code)
},
onFail: (err) => {
alert('fail')
alert(JSON.stringify(err))
}
})
})
}
}
5.在项目启动时调用上面这个方法就可以了。
本项目将此方法写在了App.vue中。
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
import { getCode } from "@/common/dingding.js";
export default {
name: "App",
created() {
//此方法就是在上面方法中定义的方法。
getCode((code) => {
//获取用户信息以及Token
this.$store.dispatch("user/getUserInfo", code);
});
},
};
</script>
6.在api.user.js文件中添加getUserInfo方法
export function getUserInfo(code) {
return request({
url: `/user/getUserInfo/${code}`,
method: 'post',
})
此时就完成了从钉钉中获取登录用户的信息。 注:该项目需要运行在钉钉Rc中(就是钉钉的环境中,似乎是这样理解)。
三、注册钉钉的回调
回调的作用是,可以在钉钉中发生某类事件后,通知我们的项目进行一系列我们自己定义的操作。
1.在钉钉项目后台注册回调地址
此地址必须是公网可以访问的地址,但是我们在开发环境中没有时,需要使用内网穿透工具帮助我们完成。 我使用的是“花生壳”。(具体使用方法请看花生壳官网,很详细)
2.注册回调地址
/**
* 应用回调信息处理
*/
@RestController
public class CallbackController {
private static final Logger mainLogger = LoggerFactory.getLogger(CallbackController.class);
private AppConfig appConfig;
private CallBackService callBackService;
@Autowired
public CallbackController(
AppConfig appConfig,
CallBackService callBackService
) {
this.appConfig = appConfig;
this.callBackService=callBackService;
}
/**
* 相应钉钉回调时的值
*/
private static final String CALLBACK_RESPONSE_SUCCESS = "success";
@RequestMapping(value = "/callback", method = RequestMethod.POST)
@ResponseBody
public Map<String, String> callback(@RequestParam(value = "signature", required = false) String signature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestBody(required = false) JSONObject json) {
String params = " signature:" + signature + " timestamp:" + timestamp + " nonce:" + nonce + " json:" + json;
try {
DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(Constant.TOKEN, Constant.ENCODING_AES_KEY,
appConfig.getAppKey());
//从post请求的body中获取回调信息的加密数据进行解密处理
String encryptMsg = json.getString("encrypt");
String plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, encryptMsg);
//处理返回的结果
callBackService.processHandle(plainText);
// 返回success的加密信息表示回调处理成功
return dingTalkEncryptor.getEncryptedMap(CALLBACK_RESPONSE_SUCCESS, System.currentTimeMillis(), Utils.getRandomStr(8));
} catch (Exception e) {
//失败的情况,应用的开发者应该通过告警感知,并干预修复
mainLogger.error("process callback failed!" + params, e);
return null;
}
}
}
本文档为本人使用时记录,避免忘记。如有错误,望指正。