文章目录
- 说明
- 一、创建钉钉应用
- 二、创建java项目
- 三、创建vue项目(或uniapp项目),npm引入sdk的依赖
- 四、拥有公网域名端口。开发环境可以使用(贝锐花生壳等工具)
- 五、打开钉钉开发者平台,配置钉钉应用的h5公网地址
- 六、打开手机钉钉,即可看到开发的页面
说明
由于钉钉开发文档的内容特别多,虽然介绍已经非常仔细了,当对于那些第一次看这个文档的时候,会有些疑惑。为了避免少走很多弯路,故写下该文章进行技术分享
- 本文主要功能:1、钉钉免登录获取用户信息 2、钉钉获取当前的定位
简单来说,就是在钉钉里面,展示我们编写的手机格式大小的网页页面
技术路线
VUE作为前端开发框架,后端为Springboot项目
可以直接通过npm运行项目或者nginx运行项目
为了方便(只需要部署一个项目),我把vue打包成为静态文件,放置到Springboot的 static 文件中。
注意
1、钉钉开发文档,有时候叫 开发H5微应用,有时候叫 开发网页应用,注意分辨
2、开发过程中,有时候会用到小程序开发者工具,注意看说明书。jsapi接口有时候这个工具用不了,得实际放到钉钉dingtalk才有用
3、目前该分享,只是涉及到网页应用,不涉及小程序应用。要注意分辨
操作步骤
1、获取钉钉的应用(corpId/agentId/appKey/appSecret)。开发环境可以自己注册企业,自己创建钉钉应用(注意配置免密的权限)
2、创建java项目,pom引入钉钉的sdk
3、创建vue项目(或uniapp项目),npm引入sdk的依赖
4、拥有公网域名端口。开发环境可以使用(贝锐花生壳等工具)
5、打开钉钉开发者平台,配置钉钉应用的h5公网地址
6、打开手机钉钉,即可看到开发的页面
思路图
获取免登录
jsapi鉴权获取定位坐标(只有安卓端 或 苹果端有用)
一、创建钉钉应用
注册钉钉企业,打开钉钉开发者平台
https://open-dev.dingtalk.com/
记录下 corpId
![](https://i-blog.csdnimg.cn/direct/151d3c6aabc34156b240e026e39c4fe5.png)
创建应用
![](https://i-blog.csdnimg.cn/direct/0b53c5de7d5b4e9cb6a753ea91158fc7.png)
记录下 agentId、appKey、appSecret
![](https://i-blog.csdnimg.cn/direct/14782afef3214b91bd19250d3e30b39d.png)
二、创建java项目
POM引入依赖,因为钉钉的接口分为新的接口和旧的接口,目前最新的版本,新接口和旧接口都是可以使用的。所以两个接口的依赖同时引入
参考我上传到 gitee的后端代码
https://gitee.com/chencanzhan/cancan-java-share/tree/master/dingtalk-demo
核心pom文件
xml
<!-- 新的接口 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.1.21</version>
</dependency>
<!-- 旧的接口 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
核心代码
java
@Service
public class DingH5Service {
@Value("${dingtalk.appKey}")
private String appKey;
@Value("${dingtalk.appSecret}")
private String accessKeySecret;
public DingUserInfo getUserByCode(String code) {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo");
OapiV2UserGetuserinfoRequest req = new OapiV2UserGetuserinfoRequest();
req.setCode(code);
OapiV2UserGetuserinfoResponse rsp = null;
try {
rsp = client.execute(req, getAccessToken());
} catch (Exception e) {
throw new RuntimeException(e);
}
cn.hutool.json.JSONObject entries = JSONUtil.parseObj(rsp.getBody());
Integer errcode = entries.getInt("errcode");
if(errcode == 0){
cn.hutool.json.JSONObject result = entries.getJSONObject("result");
DingUserInfo dingUserInfo = new DingUserInfo();
dingUserInfo.setAssociatedUnionid(result.getStr("associated_unionid"));
String unionid = result.getStr("unionid");
dingUserInfo.setUnionid(unionid);
String deviceId = result.getStr("device_id");
dingUserInfo.setDeviceId(deviceId);
dingUserInfo.setSysLevel(result.getInt("sys_level"));
String name = result.getStr("name");
dingUserInfo.setName(name);
dingUserInfo.setSys(result.getBool("sys"));
String userid = result.getStr("userid");
dingUserInfo.setUserid(userid);
return dingUserInfo;
}
return null;
}
public String getJsapiTicket() {
com.aliyun.dingtalkoauth2_1_0.Client client = null;
try {
client = createClient();
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders createJsapiTicketHeaders = new com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders();
createJsapiTicketHeaders.xAcsDingtalkAccessToken = getAccessToken();
CreateJsapiTicketResponse jsapiTicketWithOptions = client.createJsapiTicketWithOptions(createJsapiTicketHeaders, new RuntimeOptions());
CreateJsapiTicketResponseBody body = jsapiTicketWithOptions.getBody();
return body.getJsapiTicket();
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
return null;
}
/**
* 创建钉钉客户端
* @return
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public String getAccessToken() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
com.aliyun.dingtalkoauth2_1_0.Client client = new com.aliyun.dingtalkoauth2_1_0.Client(config);
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey(appKey)
.setAppSecret(accessKeySecret);
try {
return client.getAccessToken(getAccessTokenRequest).getBody().getAccessToken();
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
return null;
}
}
java
@RestController
@RequestMapping("/ding-h5")
public class DingH5Controller {
@Value("${dingtalk.agentId}")
private String agentId;
@Value("${dingtalk.corpId}")
private String corpId;
@Value("${dingtalk.appKey}")
private String appKey;
@Value("${dingtalk.urlPath}")
private String urlPath;
@Autowired
private DingH5Service dingH5Service;
/**
* 获取签名
* @param dingConfigSignVo
* @param request
* @return
*/
@PostMapping("/signAll")
public ResponseEntity<Object> signAll(@RequestBody DingConfigSignVo dingConfigSignVo, HttpServletRequest request){
String sign = null;
String signedUrl = urlPath;
String jticket = dingH5Service.getJsapiTicket();
dingConfigSignVo.setJsticket(jticket);
Map<String, Object> jMap = new HashMap<>();
try {
sign = DdConfigSign.sign(dingConfigSignVo.getJsticket(),dingConfigSignVo.getNonceStr(),dingConfigSignVo.getTimeStamp(),signedUrl);
} catch (Exception e) {
throw new RuntimeException(e);
}
jMap.put("agentId",agentId);
jMap.put("corpId",corpId);
jMap.put("appKey",appKey);
jMap.put("sign",sign);
return new ResponseEntity<>(jMap, HttpStatus.OK);
}
/**
* 根据code获取用户信息
* @param code
* @return
*/
@GetMapping("/getUserByCode")
public ResponseEntity<Object> getUserByCode(String code){
DingUserInfo userByCode = dingH5Service.getUserByCode(code);
return new ResponseEntity<>(userByCode, HttpStatus.OK);
}
}
java
/**
* 计算dd.config的签名参数
**/
public class DdConfigSign {
/**
* 计算dd.config的签名参数
*
* @param jsticket 通过微应用appKey获取的jsticket
* @param nonceStr 自定义固定字符串
* @param timeStamp 当前时间戳
* @param url 调用dd.config的当前页面URL
* @return
* @throws Exception
*/
public static String sign(String jsticket, String nonceStr, long timeStamp, String url) throws Exception {
String plain = "jsapi_ticket=" + jsticket + "&noncestr=" + nonceStr + "×tamp=" + String.valueOf(timeStamp)
+ "&url=" + decodeUrl(url);
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
sha1.reset();
sha1.update(plain.getBytes("UTF-8"));
return byteToHex(sha1.digest());
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
// 字节数组转化成十六进制字符串
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
/**
* 因为ios端上传递的url是encode过的,android是原始的url。开发者使用的也是原始url,
* 所以需要把参数进行一般urlDecode
*
* @param url
* @return
* @throws Exception
*/
private static String decodeUrl(String url) throws Exception {
URL urler = new URL(url);
StringBuilder urlBuffer = new StringBuilder();
urlBuffer.append(urler.getProtocol());
urlBuffer.append(":");
if (urler.getAuthority() != null && urler.getAuthority().length() > 0) {
urlBuffer.append("//");
urlBuffer.append(urler.getAuthority());
}
if (urler.getPath() != null) {
urlBuffer.append(urler.getPath());
}
if (urler.getQuery() != null) {
urlBuffer.append('?');
urlBuffer.append(URLDecoder.decode(urler.getQuery(), "utf-8"));
}
return urlBuffer.toString();
}
public static String getRandomStr(int count) {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
三、创建vue项目(或uniapp项目),npm引入sdk的依赖
1、使用npm安装。
npm install dingtalk-jsapi --save
2、加载 dingtalk-jsapi
import * as dd from 'dingtalk-jsapi'; // 此方式为整体加载,也可按需进行加载
完整的代码
vue
<template>
<el-main>
<div>
用户名:{{name}}
</div>
<div>
当前位置:{{rrsss.address}}
</div>
<button @click="handlegetSignAll">测试</button>
</el-main>
</template>
<script>
import api from '@/api';
import * as dd from 'dingtalk-jsapi'; // 此方式为整体加载,也可按需进行加载
export default {
data() {
return {
t1: 0,
name: '',
agentId: '',
appKey: '',
corpId: '',
sign: '',
rrsss: {},
}
},
mounted() {
this.handlegetSignAll();
},
methods: {
handlegetSignAll() {
this.t1 = Date.now()
let params = {
nonceStr: 'a',
timeStamp: this.t1
}
api.getSignAll(params).then(res => {
if (res && res.status === 200) {
this.agentId = res.data.agentId
this.appKey = res.data.appKey
this.corpId = res.data.corpId
this.sign = res.data.sign
this.setDDConfig();
this.getAuthCode();
}
})
},
setDDConfig() {
/**钉钉鉴权 */
dd.config({
agentId: this.agentId, // 必填,微应用ID
corpId: this.corpId,//必填,企业ID
timeStamp: this.t1, // 必填,生成签名的时间戳
nonceStr: 'a', // 必填,自定义固定字符串。
signature: this.sign, // 必填,签名
type: 0, //选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持
jsApiList: [
'device.geolocation.get'
] // 必填,需要使用的jsapi列表,注意:不要带dd。
})
this.getGeolocation();
//该方法必须带上,用来捕获鉴权出现的异常信息,否则不方便排查出现的问题
dd.error(function () {
console.log("钉钉鉴权失败,无法定位等,请联系管理员,或重新尝试!");
})
},
getGeolocation() {
dd.ready(() => {
dd.device.geolocation.get({
targetAccuracy: 200,
coordinate: 1,
withReGeocode: true,
useCache: false,
onSuccess: function (res) {
// 调用成功时回调
console.log(res)
this.rrsss = res
},
onFail: function (err) {
// 调用失败时回调
console.log(err)
}
});
})
},
getAuthCode() {
dd.requestAuthCode({
corpId: this.corpId,
clientId: this.appKey,
onSuccess: (result) => {
api.getUserInfo({code:result.code}).then(res => {
if (res && res.status === 200) {
this.name = res.data.name
}
})
},
onFail: function () { },
});
}
}
}
</script>
<style scoped>
</style>
四、拥有公网域名端口。开发环境可以使用(贝锐花生壳等工具)
这自己百度,映射到本地端口
可以直接通过npm运行项目或者nginx运行项目
为了方便(只需要部署一个项目),我把vue打包成为静态文件,放置到Springboot的 static 文件中。
五、打开钉钉开发者平台,配置钉钉应用的h5公网地址
选择添加应用能力
![](https://i-blog.csdnimg.cn/direct/7e6145e1b78844d696884367a7cc2b73.png)
填写公网域名
![](https://i-blog.csdnimg.cn/direct/9261d0aa549e4b57b07c00c33f8ae29a.png)
同时记得开放权限
![](https://i-blog.csdnimg.cn/direct/7ee273c169644b01b29eb7519b73f2e3.png)
六、打开手机钉钉,即可看到开发的页面
![](https://i-blog.csdnimg.cn/direct/d9346b78f7e84642b0809f0e477d0324.png)