钉钉H5微应用Springboot+Vue开发分享

文章目录

说明

由于钉钉开发文档的内容特别多,虽然介绍已经非常仔细了,当对于那些第一次看这个文档的时候,会有些疑惑。为了避免少走很多弯路,故写下该文章进行技术分享

  • 本文主要功能: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

创建应用

记录下 agentId、appKey、appSecret

二、创建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 + "&timestamp=" + 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公网地址

选择添加应用能力

填写公网域名

同时记得开放权限

六、打开手机钉钉,即可看到开发的页面

相关推荐
XMYX-04 小时前
Spring Boot + Prometheus 实现应用监控(基于 Actuator 和 Micrometer)
spring boot·后端·prometheus
@yanyu6665 小时前
springboot实现查询学生
java·spring boot·后端
酷爱码6 小时前
Spring Boot项目中JSON解析库的深度解析与应用实践
spring boot·后端·json
java干货7 小时前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
武昌库里写JAVA10 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
小白杨树树10 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
clk660716 小时前
Spring Boot
java·spring boot·后端
爱敲代码的TOM17 小时前
基于JWT+SpringSecurity整合一个单点认证授权机制
spring boot