赞
踩
使用钉钉进行H5网页开发的时候,需要调用一些钉钉提供具有原生能力的api,要调用这些api需要进行jsapi授权。
详见官方文档(可选)开发网页应用前端 - 钉钉开放平台 (dingtalk.com)
官方只提供了java和php的demo,并没有提供nodejs版本的后端权限方案,所以自己实现了一下
官方提供的步骤大致分为四个步骤(请务必阅读官方文档)
我将代码分为两部分,一部分是前端,一部分是后端(nodejs)
解释一下,下面的代码干了啥,当页面加载完成的时候,向后端
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, // 必填,微应用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>
index.js
,核心授权代码在utils/sign.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") })
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') } }
{
"AppKey": "xxx",
"AppSecret": "xxx",
"AgentId": "xx",
"CorpId": "xxx"
}
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; } }
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; } }
// 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。