钉钉开发网页应用JSAPI前端授权鉴权nodejs实现

red润 2024-09-17 08:03:01 阅读 61

钉钉开发网页应用JSAPI前端授权鉴权nodejs实现

使用钉钉进行H5网页开发的时候,需要调用一些钉钉提供具有原生能力的api,要调用这些api需要进行jsapi授权。

详见官方文档(可选)开发网页应用前端 - 钉钉开放平台 (dingtalk.com)

官方只提供了java和php的demo,并没有提供nodejs版本的后端权限方案,所以自己实现了一下

官方提供的步骤大致分为四个步骤(请务必阅读官方文档

获取token 我们将会实现token缓存,过期自动更新获取jsapiTicket 我们将会实现ticket缓存,过期自动更新计算签名 使用sha1包进行签名使用官方sdk进行权限校验 前端调用sdk进行权限校验

我将代码分为两部分,一部分是前端,一部分是后端(nodejs)

前端实现,这里使用vue3演示

解释一下,下面的代码干了啥,当页面加载完成的时候,向后端<code>http://192.168.1.63:3000/jsSdkAuthorized接口发送请求(后端代码将实现这个接口),并携带url参数,后端将拿到url做处理,最终返回授权结果,并进行验证,这里对应第4步骤

<script setup lang="ts">code>

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>code>

</template>

<style scoped lang="scss">code>

.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}&timestamp=${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

写在最后!官方文档没有提供nodejs代码,差评,提供的文档不够详细,差评。还是前端不够被重视,认为后端就是java或php才能干。。。。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。