钉钉开发网页应用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步骤

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}&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才能干。。。。

相关推荐
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#