钉钉开发网页应用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才能干。。。。

相关推荐
码客前端4 分钟前
理解 Flex 布局中的 flex:1 与 min-width: 0 问题
前端·css·css3
Komorebi゛4 分钟前
【CSS】圆锥渐变流光效果边框样式实现
前端·css
工藤学编程17 分钟前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js
徐同保17 分钟前
上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片
前端·javascript·pdf
怕浪猫18 分钟前
React从入门到出门第四章 组件通讯与全局状态管理
前端·javascript·react.js
欧阳天风25 分钟前
用setTimeout代替setInterval
开发语言·前端·javascript
EndingCoder29 分钟前
箭头函数和 this 绑定
linux·前端·javascript·typescript
郑州光合科技余经理29 分钟前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活
沐墨染32 分钟前
大型数据分析组件前端实践:多维度检索与实时交互设计
前端·elementui·数据挖掘·数据分析·vue·交互
xkxnq35 分钟前
第一阶段:Vue 基础入门(第 11 天)
前端·javascript·vue.js