钉钉开发网页应用JSAPI前端授权鉴权nodejs实现
使用钉钉进行H5网页开发的时候,需要调用一些钉钉提供具有原生能力的api,要调用这些api需要进行jsapi授权。
详见官方文档(可选)开发网页应用前端 - 钉钉开放平台 (dingtalk.com)
官方只提供了java和php的demo,并没有提供nodejs版本的后端权限方案,所以自己实现了一下
官方提供的步骤大致分为四个步骤(请务必阅读官方文档)
- 获取token 我们将会实现token缓存,过期自动更新
- 获取jsapiTicket 我们将会实现ticket缓存,过期自动更新
- 计算签名 使用sha1包进行签名
- 使用官方sdk进行权限校验 前端调用sdk进行权限校验
我将代码分为两部分,一部分是前端,一部分是后端(nodejs)
前端实现,这里使用vue3演示
解释一下,下面的代码干了啥,当页面加载完成的时候,向后端
http://192.168.1.63:3000/jsSdkAuthorized
接口发送请求(后端代码将实现这个接口),并携带url参数,后端将拿到url做处理,最终返回授权结果,并进行验证,这里对应第4步骤
html
<script setup lang="ts">
import { onMounted } from 'vue';
import axios from 'axios';
import * as dd from 'dingtalk-jsapi';
onMounted(async () => {
let resConfig: any = await axios({
headers: {
'Content-Type': 'application/json'
},
method: 'get',
url: 'http://192.168.1.63:3000/jsSdkAuthorized',
params: {
url: location.href.split('#')[0]
}
});
// console.log(location);
if (resConfig.data.code == 200) {
let { agentId, corpId, timeStamp, nonceStr, signature } = resConfig.data.signatureObj;
console.log('signatureObj', agentId, corpId, timeStamp, nonceStr, signature);
dd.config({
agentId, // 必填,微应用ID
corpId, //必填,企业ID
timeStamp, // 必填,生成签名的时间戳
nonceStr, // 必填,自定义固定字符串。
signature, // 必填,签名
type: 0, //选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持
jsApiList: ['biz.contact.choose'] // 必填,需要使用的jsapi列表,注意:不要带dd。
});
dd.ready(() => {
console.log('ok');
});
dd.error(function (err) {
console.log('dd error: ' + JSON.stringify(err));
}); //该方法必须带上,用来捕获鉴权出现的异常信息,否则不方便排查出现的问题
}
});
</script>
<template>
<div class="container">red润</div>
</template>
<style scoped lang="scss">
.container {
background-color: red;
}
</style>
后端实现 这里使用express框架 (代码较多,主入口文件在index.js
,核心授权代码在utils/sign.js
中)
index.js后端主入口
解释下面的代码,
- 后端收到前端发来的请求
app.get("/jsSdkAuthorized")
- 解析参数
- 执行步骤1获取token
- 执行步骤2获取ticket
- 执行步骤3签名
- 。。。
import express from 'express'
import cors from 'cors'
import config from "./datas/config.json" assert {type: "json"}
import { getAccessToken } from './utils/getAccessToken.js'
import { getRandomStr, sign } from './utils/sign.js'
import { getTicket } from './utils/getTicket.js'
const app = express()
const port = 3000
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.get("/jsSdkAuthorized", async (req, res) => {
// 解析参数
let url = req.query.url;
// 步骤1
let token = await getAccessToken();
// 步骤2
let jsapiTicket = await getTicket(token);
// 应用id前端发送
let agentId = config.AgentId;
let corpId = config.CorpId;
let timeStamp = Date.now();
// let nonceStr = getRandomStr(16)
let nonceStr = getRandomStr(16)
// 步骤3
let signature = sign(jsapiTicket, nonceStr, timeStamp, url);
res.send({
code: 200,
signatureObj: {
agentId,
corpId,
timeStamp,
nonceStr,
signature
}
})
})
app.listen(port, () => {
console.log(port + ":running")
})
api/index.js 后端发送的请求
import axios from "axios";
const BASE_URL = "https://api.dingtalk.com/v1.0/oauth2";
/**
* 获取token
* @param {*} appKey
* @param {*} appSecret
* @returns
*/
export const accessToken = async (appKey, appSecret) => {
let data = await axios({
headers: {
'Content-Type': 'application/json'
},
method: 'post',
url: `${BASE_URL}/accessToken`,
data: {
appKey,
appSecret
}
});
return data.data
}
/**
* 获取jsapiTicket
* @param {*} token
* @returns
*/
export const jsapiTicket = async (token) => {
try {
let data = await axios({
headers: {
'Content-Type': 'application/json',
'x-acs-dingtalk-access-token': token
},
method: 'post',
url: `${BASE_URL}/jsapiTickets`,
data: {}
});
return data.data
} catch (error) {
console.log(error, 'error')
}
}
datas/config.json 配置参数
{
"AppKey": "xxx",
"AppSecret": "xxx",
"AgentId": "xx",
"CorpId": "xxx"
}
utils/getAccessToken.js 获取token,并且缓存
import fs from 'fs';
import { fileURLToPath } from 'url';
import path from 'path';
// 只读,不修改
import config from '../datas/config.json' assert {type: "json"}
import { accessToken } from '../api/index.js';
const appKey = config.AppKey;
const appSecret = config.AppSecret;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// console.log(__filename, __dirname, '__filename,__dirname')
export const getAccessToken = async () => {
// 判断当前token是否存在,如果存在就获取当前的token,如果存在,但是过期了,就重新生成token,如果没有token,那也重新生成token
// 获取当前的时间
let currentTime = Date.now();
// 获取本地的存放的accesstoken
let accessTokenJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../datas/token.json")));
// 如果失效,重新请求
if (accessTokenJson.accessToken == '' || accessTokenJson.expireIn < currentTime) {
console.log("token失效");
// 获取新的token
console.log("get remote: token");
let data = await accessToken(appKey, appSecret);
accessTokenJson.accessToken = data.accessToken;
// expires_in单位秒 5分钟
accessTokenJson.expireIn = Date.now() + (data.expireIn - 300) * 1000;
fs.writeFileSync(path.resolve(__dirname, "../datas/token.json"), JSON.stringify(accessTokenJson));
return accessTokenJson.accessToken
} else {// 从本地获取
console.log("get local: token");
return accessTokenJson.accessToken;
}
}
utils/getTicket.js 获取ticket并且缓存
import fs from 'fs';
import { fileURLToPath } from 'url';
import path from 'path';
// 只读,不修改
import { jsapiTicket } from '../api/index.js'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// console.log(__filename, __dirname, '__filename,__dirname')
export const getTicket = async (token) => {
// 判断当前ticket是否存在,如果存在就获取当前的ticket,如果存在,但是过期了,就重新生成ticket,如果没有ticket,那也重新生成ticket
// 获取当前的时间
let currentTime = Date.now();
// 获取本地的存放的accessticket
let accessTicket = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../datas/ticket.json")));
// 如果失效,重新请求
if (accessTicket.jsapiTicket == '' || accessTicket.expireIn < currentTime) {
console.log("ticket失效");
// 获取新的ticket
console.log("get remote: ticket");
let data = await jsapiTicket(token);
accessTicket.jsapiTicket = data.jsapiTicket;
// expires_in单位秒 5分钟
accessTicket.expireIn = Date.now() + (data.expireIn - 300) * 1000;
fs.writeFileSync(path.resolve(__dirname, "../datas/ticket.json"), JSON.stringify(accessTicket));
return accessTicket.jsapiTicket
} else {// 从本地获取
console.log("get local: ticket");
return accessTicket.jsapiTicket;
}
}
utils/sign.js核心鉴权函数
// import CryptoJS from 'crypto-js'
// import crypto from 'crypto'
import sha1 from 'sha1'
/**
* 计算dd.config的签名参数
*
* @param {string} jsticket 通过微应用appKey获取的jsticket
* @param {string} nonceStr 自定义固定字符串
* @param {number} timeStamp 当前时间戳
* @param {string} currentUrl 调用dd.config的当前页面URL
* @returns {string}
*/
export const sign = (ticket, nonce, timeStamp, url) => {
let plainTex = `jsapi_ticket=${ticket}&noncestr=${nonce}×tamp=${timeStamp}&url=${decodeURIComponent(url)}`;
let signature = sha1(plainTex);
return signature;
}
/**
* 生成随机字符串
*
* @param {number} count 随机字符串长度
* @returns {string}
*/
export const getRandomStr = (count) => {
const base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < count; i++) {
const randomIndex = Math.floor(Math.random() * base.length);
result += base[randomIndex];
}
return result;
}
/**
* 返回随机字符串
* @returns
*/
export const getNonceStr = () => {
return Math.random().toString(16).substring(2, 15)
}
最终效果
前端控制台输出
ok