当前位置: 首页 > news >正文

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

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

使用钉钉进行H5网页开发的时候,需要调用一些钉钉提供具有原生能力的api,要调用这些api需要进行jsapi授权。
详见官方文档(可选)开发网页应用前端 - 钉钉开放平台 (dingtalk.com)
官方只提供了java和php的demo,并没有提供nodejs版本的后端权限方案,所以自己实现了一下

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

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

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

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

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

<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, // 必填,微应用IDcorpId, //必填,企业IDtimeStamp, // 必填,生成签名的时间戳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 = 3000app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: false }))app.get("/jsSdkAuthorized", async (req, res) => {
// 解析参数let url = req.query.url;// 步骤1let token = await getAccessToken();// 步骤2let 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)// 步骤3let 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();// 获取本地的存放的accesstokenlet accessTokenJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../datas/token.json")));// 如果失效,重新请求if (accessTokenJson.accessToken == '' || accessTokenJson.expireIn < currentTime) {console.log("token失效");// 获取新的tokenconsole.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();// 获取本地的存放的accessticketlet accessTicket = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../datas/ticket.json")));// 如果失效,重新请求if (accessTicket.jsapiTicket == '' || accessTicket.expireIn < currentTime) {console.log("ticket失效");// 获取新的ticketconsole.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才能干。。。。

相关文章:

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

钉钉开发网页应用JSAPI前端授权鉴权nodejs实现 使用钉钉进行H5网页开发的时候&#xff0c;需要调用一些钉钉提供具有原生能力的api&#xff0c;要调用这些api需要进行jsapi授权。 详见官方文档&#xff08;可选&#xff09;开发网页应用前端 - 钉钉开放平台 (dingtalk.com) 官方…...

uniapp 自定义全局弹窗

自定义全局弹窗可在js和.vue文件中调用&#xff0c;unipop样式不满足&#xff0c;需自定义样式。 效果图 目录结构 index.vue <template><view class"uni-popup" v-if"isShow"><view class"uni-popup__mask uni-center ani uni-cust…...

element+-ui图片无法使用--安装

element-ui图片无法使用 安装npm install element-plus/icons-vue 注册 // main.jsimport * as ElementPlusIconsVue from element-plus/icons-vueconst app createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, compo…...

Python编码系列—Python ORM(对象关系映射):高效数据库编程实践

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…...

一次日志记录中使用fastjson涉及到ByteBuffer的教训

背景 目前本人在公司负责的模块中&#xff0c;有一个模块是负责数据同步的&#xff0c;主要是将我们数据产线使用的 AWS Dynamodb 同步的我们的测试QA 的环境的 MongoDB 的库中&#xff0c;去年开始也提供了使用 EMR 批量同步的功能&#xff0c;但是有时候业务也需要少量的数据…...

掌握TCP连接管理与流量控制:从零开始

文章目录 1. TCP连接管理1.1 三次握手&#xff08;Three-way Handshake&#xff09;1.2 四次挥手&#xff08;Four-way Handshake&#xff09;1.3 TCP连接管理的重要性 2. TCP流量控制2.1 滑动窗口&#xff08;Sliding Window&#xff09;2.2 拥塞控制&#xff08;Congestion C…...

python提取b站视频的音频(提供源码

如果我想开一家咖啡厅&#xff0c;那么咖啡厅的音乐可得精挑细选&#xff01;又假设我非常喜欢o叔&#xff0c;而o叔只在b站弹钢琴&#xff0c;那这时候我就得想方设法把b站的视频转为音频咯&#xff01; 一、首先打开网页版bilibili&#xff0c;按F12&#xff1a; 二、刷新页面…...

嵌入式Linux ,QT5 鼠标键盘设备参数指定环境变量的方法

根文件系统中&#xff0c;一般用mdev来管理设备&#xff0c;不像udev方便&#xff0c;有时候在执行rcS脚本的时候因为&#xff0c;太快&#xff0c;有些设备比如鼠标还没在/dev/input中生成设备文件&#xff0c;最好使用前用mdev -s扫描并等待几秒钟&#xff0c;然后就可以在in…...

C语言钥匙迷宫2.0

目录 开头程序程序的流程图程序游玩的效果结尾 开头 大家好&#xff0c;我叫这是我58。废话不多说&#xff0c;咱们直接开始。 程序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <string.h> #include <Windows.h> enum color {Y,B,R …...

【多线程】初步认识Thread类及其应用

&#x1f490;个人主页&#xff1a;初晴~ &#x1f4da;相关专栏&#xff1a;多线程 / javaEE初阶 上篇文章我们简单介绍了什么是进程与线程&#xff0c;以及他们之间的区别与联系&#xff0c;实际应用中还是以多线程编程为主的&#xff0c;所以这篇文章就让我们更加深入地去剖…...

algorithm算法库学习之——划分操作和排序操作

algorithm此头文件是算法库的一部分。本篇介绍划分操作和排序操作。 划分操作 is_partitioned (C11) 判断范围是否已按给定的谓词划分 (函数模板) partition 将范围中的元素分为两组 (函数模板) partition_copy (C11) 复制一个范围&#xff0c;将各元素分为两组 (函数模板) st…...

XSS实验记录

目录 XXS地址 实验过程 Ma Spaghet Jeff Ugandan Knuckles Ricardo Milos Ah Thats Hawt Ligma Mafia Ok, Boomer XXS地址 XSS Game - Learning XSS Made Simple! | Created by PwnFunction 实验过程 Ma Spaghet 要求我们弹出一个alert(1337)sandbox.pwnfuncti…...

Cortex-A7的GIC(全局中断控制器)使用方法(7):基于stm32MP135的GIC配置中断效果测试

0 参考资料 STM32MP13xx参考手册.pdf&#xff08;RM0475&#xff09; ARM Generic Interrupt Controller Architecture version 2.0 - Architecture Specification.pdf 1 GIC配置中断效果测试 前面我们已经实现了GIC的配置&#xff0c;为了验证GIC是否配置有效&#xff0c;本例…...

c++动态数组new和delete

文章目录 动态数组的使用大全1. **基本创建和初始化**2. **动态调整大小**3. **动态数组的使用与标准库 std::vector**4. **动态数组作为函数参数**输出 5. **使用动态数组存储用户输入** 动态数组的使用大全 1. 基本创建和初始化 示例&#xff1a; #include <iostream&g…...

Redis热点知识速览(redis的数据结构、高性能、持久化、主从复制、集群、缓存淘汰策略、事务、Pub/Sub、锁机制、常见问题等)

Redis是一个开源的、使用内存作为存储的、支持数据结构丰富的NoSQL数据库。它的高性能、灵活性和简单易用使其在许多场景下成为首选的缓存解决方案。以下是Redis的常见和热点知识总结。 数据结构 Redis支持五种基本数据结构&#xff1a; String&#xff1a;字符串是Redis中最…...

【C++浅析】lambda表达式:基本结构 使用示例

基本结构 [捕获列表](参数列表) -> 返回类型 { // 函数体 } 捕获列表 ([ ]): 用于指定外部变量的捕获方式。可以&#xff1a; 通过值捕获&#xff1a;[x]通过引用捕获&#xff1a;[&x]捕获所有变量通过值&#xff1a;[]捕获所有变量通过引用&#xff1a;[&]自…...

利用Redis获取权限的多种方式

更多实战内容&#xff0c;可前往无问社区查看http://www.wwlib.cn/index.php/artread/artid/10333.html Redis是我们在实战中经常接触到的一款数据库&#xff0c;因其在前期打点中被利用后可直接影响服务器安全所以在攻防过程中也备受红队关注&#xff0c;在本文中会重点分享一…...

LeetCode - LCR 146- 螺旋遍历二维数组

LCR 146题 题目描述&#xff1a; 给定一个二维数组 array&#xff0c;请返回「螺旋遍历」该数组的结果。 螺旋遍历&#xff1a;从左上角开始&#xff0c;按照 向右、向下、向左、向上 的顺序 依次 提取元素&#xff0c;然后再进入内部一层重复相同的步骤&#xff0c;直到提取完…...

如何获取Bing站长工具API密钥

Bing站长工具近期悄然上线了网站URL推送功能&#xff0c;似乎有意跟随百度的步伐。这个新功能允许站长通过API向Bing提交链接数据&#xff0c;当然也可以通过Bing站长工具手动提交。 本文将详细介绍如何通过Bing站长工具生成用于网站链接推送的API密钥。 首先&#xff0c;访问…...

NC 调整数组顺序使奇数位于偶数前面(一)

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 输入一个长度…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用

文章目录 一、背景知识&#xff1a;什么是 B-Tree 和 BTree&#xff1f; B-Tree&#xff08;平衡多路查找树&#xff09; BTree&#xff08;B-Tree 的变种&#xff09; 二、结构对比&#xff1a;一张图看懂 三、为什么 MySQL InnoDB 选择 BTree&#xff1f; 1. 范围查询更快 2…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器

一、原理介绍 传统滑模观测器采用如下结构&#xff1a; 传统SMO中LPF会带来相位延迟和幅值衰减&#xff0c;并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF)&#xff0c;可以去除高次谐波&#xff0c;并且不用相位补偿就可以获得一个误差较小的转子位…...

深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏

一、引言 在深度学习中&#xff0c;我们训练出的神经网络往往非常庞大&#xff08;比如像 ResNet、YOLOv8、Vision Transformer&#xff09;&#xff0c;虽然精度很高&#xff0c;但“太重”了&#xff0c;运行起来很慢&#xff0c;占用内存大&#xff0c;不适合部署到手机、摄…...

JS红宝书笔记 - 3.3 变量

要定义变量&#xff0c;可以使用var操作符&#xff0c;后跟变量名 ES实现变量初始化&#xff0c;因此可以同时定义变量并设置它的值 使用var操作符定义的变量会成为包含它的函数的局部变量。 在函数内定义变量时省略var操作符&#xff0c;可以创建一个全局变量 如果需要定义…...

Xcode 16 集成 cocoapods 报错

基于 Xcode 16 新建工程项目&#xff0c;集成 cocoapods 执行 pod init 报错 ### Error RuntimeError - PBXGroup attempted to initialize an object with unknown ISA PBXFileSystemSynchronizedRootGroup from attributes: {"isa">"PBXFileSystemSynchro…...